├── .editorconfig ├── .gitignore ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── RELEASE_NOTES.md ├── bridge ├── Cargo.toml ├── build.rs └── src │ ├── api.rs │ ├── app.rs │ ├── bridge │ ├── balance.rs │ ├── chain_id.rs │ ├── deploy.rs │ ├── deposit_relay.rs │ ├── gas_price.rs │ ├── mod.rs │ ├── nonce.rs │ ├── withdraw_confirm.rs │ └── withdraw_relay.rs │ ├── config.rs │ ├── contracts.rs │ ├── database.rs │ ├── error.rs │ ├── lib.rs │ ├── macros.rs │ ├── message_to_mainnet.rs │ ├── signature.rs │ ├── transaction.rs │ └── util.rs ├── cli ├── Cargo.toml ├── build.rs └── src │ └── main.rs ├── contracts ├── BridgeableToken.sol ├── README.md └── bridge.sol ├── examples ├── config.toml └── supervisor ├── integration-tests ├── Cargo.toml ├── bridge_config.toml ├── bridge_config_gas_price.toml ├── build.rs ├── keys │ ├── authority.json │ └── user.json ├── password.txt └── tests │ ├── basic_deposit_then_withdraw.rs │ └── insufficient_funds.rs ├── res ├── deposit.png └── withdraw.png ├── tests ├── Cargo.toml ├── src │ └── lib.rs └── tests │ ├── deposit_relay.rs │ ├── log_stream.rs │ ├── withdraw_confirm.rs │ └── withdraw_relay.rs ├── tools ├── estimate_gas_costs.sh └── solc_compile.sh └── truffle ├── .node-xmlhttprequest-content-37792 ├── .node-xmlhttprequest-sync-37792 ├── .soliumignore ├── .soliumrc.json ├── contracts ├── Migrations.sol └── bridge.sol ├── migrations └── 1_initial_migration.js ├── package.json ├── test ├── foreign-erc20.js ├── foreign.js ├── helpers.js ├── helpers │ └── helpers.js ├── home.js ├── message.js └── message_signing.js ├── truffle.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | indent_style=tab 4 | indent_size=tab 5 | tab_width=4 6 | end_of_line=lf 7 | charset=utf-8 8 | trim_trailing_whitespace=true 9 | max_line_length=120 10 | insert_final_newline=true 11 | 12 | [*.sol] 13 | indent_style=space 14 | indent_size=4 15 | tab_size=4 16 | 17 | [*.js] 18 | indent_style=space 19 | indent_size=2 20 | tab_size=2 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | *.swp 6 | *.swo 7 | *.swn 8 | *.DS_Store 9 | 10 | # Visual Studio Code stuff 11 | /.vscode 12 | 13 | # GitEye stuff 14 | /.project 15 | 16 | # idea ide 17 | .idea 18 | 19 | # truffle stuff 20 | truffle/build 21 | 22 | examples/parity_ora/signer 23 | examples/parity_ora/cache 24 | examples/parity_ora/chains 25 | examples/parity_ora/dapps 26 | examples/parity_ora/network 27 | examples/parity_ora-2/signer 28 | examples/parity_ora-2/cache 29 | examples/parity_ora-2/chains 30 | examples/parity_ora-2/dapps 31 | examples/parity_ora-2/network 32 | examples/parity_POA/cache 33 | examples/parity_POA/chains 34 | examples/parity_POA/dapps 35 | examples/parity_POA/network 36 | examples/parity_kov/cache 37 | examples/parity_kov/chains 38 | examples/parity_kov/dapps 39 | examples/parity_kov/network 40 | examples/parity_kov/keys/kovan/dapps_history.json 41 | examples/parity_kov/keys/kovan/address_book.json 42 | examples/parity_rop/cache 43 | examples/parity_rop/chains 44 | examples/parity_rop/dapps 45 | examples/parity_rop/network 46 | examples/parity_rop/keys/kovan/dapps_history.json 47 | examples/parity_rop/keys/kovan/address_book.json 48 | examples/parity_ora/keys/OraclesPoA/dapps_history.json 49 | examples/parity_ora/keys/OraclesPoA/address_book.json 50 | examples/parity_ora-2/keys/OraclesPoA/dapps_history.json 51 | examples/parity_ora-2/keys/OraclesPoA/address_book.json 52 | jsTests/node_modules/ 53 | examples/db.toml 54 | jsTests/package-lock.json 55 | node_modules 56 | compiled_contracts 57 | 58 | integration-tests/tmp 59 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | branches: 3 | only: 4 | - master 5 | git: 6 | depth: false 7 | matrix: 8 | include: 9 | - language: rust 10 | rust: stable 11 | cache: cargo 12 | fast_finish: false 13 | before_script: 14 | - sudo add-apt-repository ppa:ethereum/ethereum -y 15 | - sudo apt-get update -y 16 | - sudo apt-get install solc libudev-dev libstdc++6 -y 17 | - sudo apt-get --yes install snapd 18 | - sudo snap install parity --stable 19 | - export PATH=/snap/bin:${PATH} 20 | script: 21 | - cd integration-tests && env BACKTRACE=1 cargo test --release -- --test-threads=1 --nocapture && cd .. 22 | - make 23 | - cp target/release/bridge bridge-linux-x86_64 24 | before_deploy: git tag `git describe --always --tags` 25 | deploy: 26 | provider: releases 27 | api_key: 28 | secure: jgCih2ejsRvdIYLFCdNn7HGNykQji+q20EAqFbK0cVXTpuQwWlLZM/TPXnkm7Rm8bG8cs+ccw2z/hnxLffpPKqzyma/agbpSW911ReI2cxmslmw4/aKY2wk9Q714C6xZ5r7JmEHV43fdQqOUUe+w3EB4StWy/6hZWdjeLHijyOtn0wAbUSYXVt/D9nNYXmSF1TQ0AogDkdz31kCwmzi7RFwlFDhg22IK3CJmeBPonti8tCCpslKqEh7ni+q/6KKzdatC0DoQshjwQTRfTDIWO+MNEPmg+7yEQC6w/vI7VXsr0NIgwqRfhjMvHqVvzUaORzBi48Hj8Y6dWFTu5a8v7dCYzuEtBchdzpg4EwRO0OxoIC+RiHooIF5OSKPjZsJhynRMe0Q4ugWf6Ytzi2/cIsdqSX3fbIkZHnEtSjxZo4GdCwb7cT2BpYSCwZRL6NkHMhUtfYeUGkCI4Cmwwz8i3jKU2B9Q/pBJgD4d8wewb/Vdb9RqjVLP8Ogjv/b1mJydr+PPs1FNjX4rXIwFrU7HhHoIQoRmkWjEW305rixwxHv3ka60rglLDPbn+Fzfv5AeFeVJoInijx2qCnUBPpU1VIa3d6hWf/460ltUTvx5F1HXG/RTZm5aWbF2Rn/gQPHpbGWT+v2isbNLqotSP83YMWA09EXTgc4uYHfO/xp4BhI= 29 | file: bridge-linux-x86_64 30 | on: 31 | repo: poanetwork/poa-bridge 32 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["tests", "cli", "bridge", "integration-tests"] 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: target/release/bridge 2 | 3 | .PHONY: target/release/bridge 4 | 5 | target/release/bridge: 6 | cd cli && cargo build --release 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # POA-Ethereum Bridge 2 | 3 | [![Join the chat at https://gitter.im/poanetwork/poa-bridge](https://badges.gitter.im/poanetwork/poa-bridge.svg)](https://gitter.im/poanetwork/poa-bridge?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | [![Waffle.io - Columns and their card count](https://badge.waffle.io/poanetwork/poa-bridge.svg?columns=all)](https://waffle.io/poanetwork/poa-bridge) 5 | 6 | **NOTE: This repository is not currently supported and is not used in production. Please see the Token Bridge repo at https://github.com/poanetwork/token-bridge for the current production version** 7 | 8 | 9 | Software, written in Rust, used by *POA bridge validators* to faciliate proof-of-authority based bridging of POA native coins to tokens on **another** Ethereum-based blockchain. 10 | 11 | The validators work with the POA bridge contracts to convert ether on one chain into the same amount of ERC20 tokens on the other and back. 12 | 13 | This software was designed to work in conjunction with the following projects. Current compatability is unknown. 14 | 15 | * [POA Bridge UI](https://github.com/poanetwork/bridge-ui) 16 | * [POA Bridge Smart Contracts](https://github.com/poanetwork/poa-bridge-contracts) 17 | * [POA Bridge Monitoring service](https://github.com/poanetwork/bridge-monitor) 18 | * [POA Bridge Deployment scripts](https://github.com/poanetwork/deployment-bridge) 19 | 20 | ### Functionality 21 | 22 | The bridge connects two chains (`home` and `foreign`). When a user deposits ether into the 23 | bridge contract contract on `home` they get the same amount of ERC20 tokens on `foreign`, 24 | and they can convert them back as well. 25 | 26 | #### Deposit 27 | 28 | ![deposit](./res/deposit.png) 29 | 30 | #### Withdraw 31 | 32 | ![withdraw](./res/withdraw.png) 33 | 34 | ### Difference from Parity Bridge 35 | 36 | Although the POA bridge was initially based on the [Parity Bridge](https://github.com/paritytech/parity-bridge), it 37 | was re-worked to include: 38 | * support of a gas price oracle introduced; 39 | * RPC is used instead of IPC; 40 | * sending of bridge approvals enhanced, increasing performance dramatically; 41 | * error handling improved to be compatible with Linux systemd facility; 42 | * bridge configuration parameters are fetched from bridge contracts so they don't need to be synchronized among several bridge instances; 43 | * bridge contracts were segregated into [the separate project](https://github.com/poanetwork/poa-bridge-contracts) and their deployment 44 | is independent from the Rust side of the bridge. Now bridge contracts: 45 | * are separate from ERC20 46 | * are upgradable; you don't need to re-configure bridge instances and DApps to use new version of contracts 47 | * allow a set of validators to be changed without neeeding to re-deploy thebridge contracts 48 | 49 | ### How to build 50 | 51 | Requires `rust` and `cargo`: [installation instructions.](https://www.rust-lang.org/en-US/install.html) 52 | 53 | Requires `solc` to be in `$PATH`: [installation instructions.](https://solidity.readthedocs.io/en/develop/installing-solidity.html) 54 | 55 | Assuming you've cloned the bridge (`git clone git@github.com:poanetwork/poa-bridge.git`), run 56 | 57 | ``` 58 | cd poa-bridge 59 | make 60 | ``` 61 | 62 | and install `../target/release/bridge` in your `$PATH`. 63 | 64 | ### Running 65 | 66 | ``` 67 | bridge --config config.toml --database db.toml 68 | ``` 69 | 70 | - `--config` - location of the configuration file. configuration file must exist 71 | - `--database` - location of the database file. 72 | 73 | Bridge forces TLS for RPC connections by default. However, in some limited scenarios (like local testing), 74 | this might be undesirable. In this case, you can use the `--allow-insecure-rpc-endpoints` option to allow non-TLS 75 | endpoints to be used. Ensure, however, that this option is not going to be used in production. 76 | 77 | 78 | #### Exit Status Codes 79 | 80 | | Code | Meaning | 81 | |------|----------------------| 82 | | 0 | Success | 83 | | 1 | Unknwon error | 84 | | 2 | I/O error | 85 | | 3 | Shutdown requested | 86 | | 4 | Insufficient funds | 87 | | 5 | Gas too low | 88 | | 6 | Gas price is too low | 89 | | 7 | Nonce reused | 90 | | 10 | Cannot connect | 91 | | 11 | Connection lost | 92 | | 12 | Bridge crashed | 93 | | 20 | RPC error | 94 | 95 | ### Configuration [file example](./examples/config.toml) 96 | 97 | ```toml 98 | keystore = "/path/to/keystore" 99 | 100 | [home] 101 | account = "0x006e27b6a72e1f34c626762f3c4761547aff1421" 102 | password = "home_password.txt" 103 | rpc_host = "http://localhost" 104 | rpc_port = 8545 105 | required_confirmations = 0 106 | poll_interval = 5 107 | request_timeout = 60 108 | default_gas_price = 1_000_000_000 # 1 GWEI 109 | 110 | [foreign] 111 | account = "0x006e27b6a72e1f34c626762f3c4761547aff1421" 112 | password = "foreign_password.txt" 113 | rpc_host = "http://localhost" 114 | rpc_port = 9545 115 | required_confirmations = 8 116 | poll_interval = 15 117 | request_timeout = 60 118 | gas_price_oracle_url = "https://gasprice.poa.network" 119 | gas_price_speed = "instant" 120 | gas_price_timeout = 10 121 | default_gas_price = 10_000_000_000 # 10 GWEI 122 | 123 | [authorities] 124 | 125 | [transactions] 126 | deposit_relay = { gas = 300000 } 127 | withdraw_relay = { gas = 300000 } 128 | withdraw_confirm = { gas = 300000 } 129 | ``` 130 | 131 | #### Options 132 | 133 | - `keystore` - path to a keystore directory with JSON keys 134 | 135 | #### home/foreign options 136 | 137 | - `home/foreign.account` - authority address on the home (**required**) 138 | - `home/foreign.password` - path to the file containing a password for the validator's account (to decrypt the key from the keystore) 139 | - `home/foreign.rpc_host` - RPC host (**required**) 140 | - `home/foreign.rpc_port` - RPC port (**defaults to 8545**) 141 | - `home/foreign.required_confirmations` - number of confirmations required to consider transaction final on home (default: **12**) 142 | - `home/foreign.poll_interval` - specify how often home node should be polled for changes (in seconds, default: **1**) 143 | - `home/foreign.request_timeout` - specify request timeout (in seconds, default: **3600**) 144 | - `home/foreign.gas_price_oracle_url` - the URL used to query the current gas-price for the home and foreign nodes, this service is known as the gas-price Oracle. This config option defaults to `None` if not supplied in the User's config TOML file. If this config value is `None`, no Oracle gas-price querying will occur, resulting in the config value for `home/foreign.default_gas_price` being used for all gas-prices. 145 | - `home/foreign.gas_price_timeout` - the number of seconds to wait for an HTTP response from the gas price oracle before using the default gas price. Defaults to `10 seconds`. 146 | - `home/foreign.gas_price_speed` - retrieve the gas-price corresponding to this speed when querying from an Oracle. Defaults to `fast`. The available values are: "instant", "fast", "standard", and "slow". 147 | - `home/foreign.default_gas_price` - the default gas price (in WEI) used in transactions with the home or foreign nodes. The `default_gas_price` is used when the Oracle cannot be reached. The default value is `15_000_000_000` WEI (ie. 15 GWEI). 148 | - `home/foreign.concurrent_http_requests` - the number of concurrent HTTP requests allowed in-flight (default: **64**) 149 | 150 | #### transaction options 151 | 152 | - `transaction.deposit_relay.gas` - specify how much gas should be consumed by deposit relay 153 | - `transaction.withdraw_confirm.gas` - specify how much gas should be consumed by withdraw confirm 154 | - `transaction.withdraw_relay.gas` - specify how much gas should be consumed by withdraw relay 155 | 156 | ### Database file format 157 | 158 | ```toml 159 | home_contract_address = "0x49edf201c1e139282643d5e7c6fb0c7219ad1db7" 160 | foreign_contract_address = "0x49edf201c1e139282643d5e7c6fb0c7219ad1db8" 161 | checked_deposit_relay = 120 162 | checked_withdraw_relay = 121 163 | checked_withdraw_confirm = 121 164 | ``` 165 | 166 | **all fields are required** 167 | 168 | - `home_contract_address` - address of the bridge contract on home chain 169 | - `foreign_contract_address` - address of the bridge contract on foreign chain 170 | - `checked_deposit_relay` - number of the last block for which an authority has relayed deposits to the foreign 171 | - `checked_withdraw_relay` - number of the last block for which an authority has relayed withdraws to the home 172 | - `checked_withdraw_confirm` - number of the last block for which an authority has confirmed withdraw 173 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | # 0.3.0 2 | 3 | In this release, dynamic gas pricing has been added 4 | (#85), allowing to fetch gas price from an external 5 | oracle. 6 | 7 | This release addresses some major performance issues 8 | with how RPC requests were coordinated. With this release, 9 | RPC requests are finally properly parallelized, leading 10 | to much better overall performance (#94) 11 | 12 | It also addresses some potential loss of database updates 13 | under certain conditions (#95) 14 | 15 | Along with this, some old configuration options were 16 | deprecated. 17 | 18 | # 0.2.1 19 | 20 | This release contains a number of bugfixes and a change in handling gas price. 21 | It is no longer set statically but rather dynamically using an external oracle 22 | (see [config example](examples/config.toml)) 23 | 24 | # 0.2.0 25 | 26 | This release, most notably, fixes a condition in which not all logs might be 27 | retrieved from an RPC endpoint, resulting in the bridge not being able to 28 | see all relevant events. 29 | 30 | It also improves the performance by introducing concurrent transaction batching. 31 | 32 | On the operations side, it'll now print the context of an occurring error 33 | before exiting to help investigating that error. 34 | 35 | # 0.1.0 36 | 37 | Initial release 38 | -------------------------------------------------------------------------------- /bridge/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bridge" 3 | version = "0.3.0" 4 | 5 | [dependencies] 6 | futures = "0.1" 7 | serde = "1.0" 8 | serde_derive = "1.0" 9 | serde_json = "1.0" 10 | tokio-core = "0.1.8" 11 | tokio-timer = "0.1.2" 12 | toml = "0.4.2" 13 | web3 = "0.3" 14 | error-chain = "0.11.0-rc.2" 15 | ethabi = "5.1" 16 | ethabi-derive = "5.0" 17 | ethabi-contract = "5.0" 18 | rustc-hex = "1.0" 19 | log = "0.3" 20 | ethereum-types = "0.3" 21 | pretty_assertions = "0.2.1" 22 | ethcore = { git = "http://github.com/paritytech/parity", rev = "991f0ca" } 23 | rlp = { git = "http://github.com/paritytech/parity", rev = "991f0ca" } 24 | keccak-hash = { git = "http://github.com/paritytech/parity", rev = "991f0ca" } 25 | ethcore-transaction = { git = "http://github.com/paritytech/parity", rev = "991f0ca" } 26 | itertools = "0.7" 27 | jsonrpc-core = "8.0" 28 | hyper = "0.11.27" 29 | hyper-tls = "0.1.3" 30 | 31 | [dev-dependencies] 32 | tempdir = "0.3" 33 | quickcheck = "0.6.1" 34 | 35 | [build-dependencies] 36 | rustc_version = "0.2.2" 37 | 38 | [features] 39 | default = [] 40 | deploy = [] 41 | -------------------------------------------------------------------------------- /bridge/build.rs: -------------------------------------------------------------------------------- 1 | extern crate rustc_version; 2 | 3 | use std::process::Command; 4 | 5 | use rustc_version::{version as get_rustc_version, Version}; 6 | 7 | fn check_rustc_version() { 8 | let minimum_required_version = Version::new(1, 26, 0); 9 | 10 | if let Ok(version) = get_rustc_version() { 11 | if version < minimum_required_version { 12 | panic!( 13 | "Invalid rustc version, `poa-bridge` requires \ 14 | rustc >= {}, found version: {}", 15 | minimum_required_version, 16 | version 17 | ); 18 | } 19 | } 20 | } 21 | 22 | fn main() { 23 | check_rustc_version(); 24 | 25 | // rerun build script if bridge contract has changed. 26 | // without this cargo doesn't since the bridge contract 27 | // is outside the crate directories 28 | println!("cargo:rerun-if-changed=../contracts/bridge.sol"); 29 | 30 | match Command::new("solc") 31 | .arg("--abi") 32 | .arg("--bin") 33 | .arg("--optimize") 34 | .arg("--output-dir").arg("../compiled_contracts") 35 | .arg("--overwrite") 36 | .arg("../contracts/bridge.sol") 37 | .status() 38 | { 39 | Ok(exit_status) => { 40 | if !exit_status.success() { 41 | if let Some(code) = exit_status.code() { 42 | panic!("`solc` exited with error exit status code `{}`", code); 43 | } else { 44 | panic!("`solc` exited because it was terminated by a signal"); 45 | } 46 | } 47 | }, 48 | Err(err) => { 49 | if let std::io::ErrorKind::NotFound = err.kind() { 50 | panic!("`solc` executable not found in `$PATH`. `solc` is required to compile the bridge contracts. please install it: https://solidity.readthedocs.io/en/develop/installing-solidity.html"); 51 | } else { 52 | panic!("an error occurred when trying to spawn `solc`: {}", err); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /bridge/src/api.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use serde::de::DeserializeOwned; 3 | use serde_json::Value; 4 | use futures::{Future, Stream, Poll}; 5 | use tokio_timer::{Timer, Interval, Timeout}; 6 | use web3::{self, api, Transport}; 7 | use web3::api::Namespace; 8 | use web3::types::{Log, Filter, H256, U256, FilterBuilder, Bytes, Address, CallRequest, BlockNumber}; 9 | use web3::helpers::{self, CallResult}; 10 | use error::{Error, ErrorKind}; 11 | 12 | /// Imperative alias for web3 function. 13 | pub use web3::confirm::send_raw_transaction_with_confirmation; 14 | 15 | /// Wrapper type for `CallResult` 16 | pub struct ApiCall { 17 | future: CallResult, 18 | message: &'static str, 19 | } 20 | 21 | impl>Future for ApiCall { 22 | type Item = T; 23 | type Error = Error; 24 | 25 | fn poll(&mut self) -> Poll { 26 | trace!(target: "bridge", "{}", self.message); 27 | self.future.poll().map_err(ErrorKind::Web3).map_err(Into::into) 28 | } 29 | } 30 | 31 | /// Imperative wrapper for web3 function. 32 | pub fn net_version(transport: T) -> ApiCall { 33 | ApiCall { 34 | future: CallResult::new(transport.execute("net_version", vec![])), 35 | message: "net_version", 36 | } 37 | } 38 | 39 | /// Imperative wrapper for web3 function. 40 | pub fn eth_get_transaction_count(transport: T, address: Address, block: Option) -> ApiCall { 41 | // we are not using Eth.balance() because it converts None block into `latest` 42 | // while we want `pending` because there might have not been enough time since 43 | // the last transaction to get it mined. 44 | let address = helpers::serialize(&address); 45 | let block = helpers::serialize(&block.unwrap_or(BlockNumber::Pending)); 46 | ApiCall { 47 | future: CallResult::new(transport.execute("eth_getTransactionCount", vec![address, block])), 48 | message: "net_version", 49 | } 50 | } 51 | 52 | 53 | use serde_json; 54 | /// trimming the null from the tail because at least some RPC servers require a topic to be present 55 | /// if there's a null 56 | /// FIXME: this is not a great fix long term 57 | fn trim_filter(filter: &Filter) -> serde_json::Value { 58 | fn trim_filter1(vals: &mut Vec) { 59 | loop { 60 | match vals.pop() { 61 | None => { 62 | return; 63 | }, 64 | Some(serde_json::Value::Null) => (), 65 | Some(v) => { 66 | vals.push(v); 67 | return; 68 | } 69 | } 70 | } 71 | } 72 | match helpers::serialize(filter) { 73 | serde_json::Value::Object(mut map) => { 74 | for (k, v) in map.iter_mut() { 75 | if k == "topics" { 76 | match v { 77 | &mut serde_json::Value::Array(ref mut v) => trim_filter1(v), 78 | _ => (), 79 | } 80 | } 81 | } 82 | serde_json::Value::Object(map) 83 | } 84 | val => val, 85 | } 86 | } 87 | 88 | /// Imperative wrapper for web3 function. 89 | pub fn logs(transport: T, filter: &Filter) -> ApiCall, T::Out> { 90 | let filter = trim_filter(filter); 91 | ApiCall { 92 | future: CallResult::new(transport.execute("eth_getLogs", vec![filter])), 93 | message: "eth_getLogs", 94 | } 95 | } 96 | 97 | /// Imperative wrapper for web3 function. 98 | pub fn block_number(transport: T) -> ApiCall { 99 | ApiCall { 100 | future: api::Eth::new(transport).block_number(), 101 | message: "eth_blockNumber", 102 | } 103 | } 104 | 105 | /// Imperative wrapper for web3 function. 106 | pub fn balance(transport: T, address: Address, block: Option) -> ApiCall { 107 | // we are not using Eth.balance() because it converts None block into `latest` 108 | // while we want `pending` because there might have not been enough time since 109 | // the last transaction to get it mined. 110 | let address = helpers::serialize(&address); 111 | let block = helpers::serialize(&block.unwrap_or(BlockNumber::Pending)); 112 | ApiCall { 113 | future: CallResult::new(transport.execute("eth_getBalance", vec![address, block])), 114 | message: "eth_getBalance", 115 | } 116 | } 117 | 118 | /// Imperative wrapper for web3 function. 119 | pub fn send_raw_transaction(transport: T, tx: Bytes) -> ApiCall { 120 | ApiCall { 121 | future: api::Eth::new(transport).send_raw_transaction(tx), 122 | message: "eth_sendRawTransaction", 123 | } 124 | } 125 | 126 | pub use bridge::nonce::send_transaction_with_nonce; 127 | 128 | /// Imperative wrapper for web3 function. 129 | pub fn call(transport: T, address: Address, payload: Bytes) -> ApiCall { 130 | let future = api::Eth::new(transport).call(CallRequest { 131 | from: None, 132 | to: address, 133 | gas: None, 134 | gas_price: None, 135 | value: None, 136 | data: Some(payload), 137 | }, None); 138 | 139 | ApiCall { 140 | future, 141 | message: "eth_call", 142 | } 143 | } 144 | 145 | /// Returns a eth_sign-compatible hash of data to sign. 146 | /// The data is prepended with special message to prevent 147 | /// chosen-plaintext attacks. 148 | pub fn eth_data_hash(mut data: Vec) -> H256 { 149 | use keccak_hash::keccak; 150 | let mut message_data = 151 | format!("\x19Ethereum Signed Message:\n{}", data.len()) 152 | .into_bytes(); 153 | message_data.append(&mut data); 154 | keccak(message_data) 155 | } 156 | 157 | /// Used for `LogStream` initialization. 158 | pub struct LogStreamInit { 159 | pub after: u64, 160 | pub filter: FilterBuilder, 161 | pub request_timeout: Duration, 162 | pub poll_interval: Duration, 163 | pub confirmations: usize, 164 | } 165 | 166 | /// Contains all logs matching `LogStream` filter in inclusive range `[from, to]`. 167 | #[derive(Debug, PartialEq)] 168 | pub struct LogStreamItem { 169 | pub from: u64, 170 | pub to: u64, 171 | pub logs: Vec, 172 | } 173 | 174 | /// Log Stream state. 175 | enum LogStreamState { 176 | /// Log Stream is waiting for timer to poll. 177 | Wait, 178 | /// Fetching best block number. 179 | FetchBlockNumber(Timeout>), 180 | /// Fetching logs for new best block. 181 | FetchLogs { 182 | from: u64, 183 | to: u64, 184 | future: Timeout, T::Out>>, 185 | }, 186 | /// All logs has been fetched. 187 | NextItem(Option), 188 | } 189 | 190 | /// Creates new `LogStream`. 191 | pub fn log_stream(transport: T, timer: Timer, init: LogStreamInit) -> LogStream { 192 | LogStream { 193 | transport, 194 | interval: timer.interval(init.poll_interval), 195 | timer, 196 | state: LogStreamState::Wait, 197 | after: init.after, 198 | filter: init.filter, 199 | confirmations: init.confirmations, 200 | request_timeout: init.request_timeout, 201 | } 202 | } 203 | 204 | /// Stream of confirmed logs. 205 | pub struct LogStream { 206 | transport: T, 207 | timer: Timer, 208 | interval: Interval, 209 | state: LogStreamState, 210 | after: u64, 211 | filter: FilterBuilder, 212 | confirmations: usize, 213 | request_timeout: Duration, 214 | } 215 | 216 | impl Stream for LogStream { 217 | type Item = LogStreamItem; 218 | type Error = Error; 219 | 220 | fn poll(&mut self) -> Poll, Self::Error> { 221 | loop { 222 | let next_state = match self.state { 223 | LogStreamState::Wait => { 224 | let _ = try_stream!(self.interval.poll()); 225 | LogStreamState::FetchBlockNumber(self.timer.timeout(block_number(&self.transport), self.request_timeout)) 226 | }, 227 | LogStreamState::FetchBlockNumber(ref mut future) => { 228 | let last_block = try_ready!(future.poll()).low_u64(); 229 | let last_confirmed_block = last_block.saturating_sub(self.confirmations as u64); 230 | if last_confirmed_block > self.after { 231 | let from = self.after + 1; 232 | let filter = self.filter.clone() 233 | .from_block(from.into()) 234 | .to_block(last_confirmed_block.into()) 235 | .build(); 236 | LogStreamState::FetchLogs { 237 | from: from, 238 | to: last_confirmed_block, 239 | future: self.timer.timeout(logs(&self.transport, &filter), self.request_timeout), 240 | } 241 | } else { 242 | LogStreamState::Wait 243 | } 244 | }, 245 | LogStreamState::FetchLogs { ref mut future, from, to } => { 246 | let logs = try_ready!(future.poll()); 247 | let item = LogStreamItem { 248 | from, 249 | to, 250 | logs, 251 | }; 252 | 253 | self.after = to; 254 | LogStreamState::NextItem(Some(item)) 255 | }, 256 | LogStreamState::NextItem(ref mut item) => match item.take() { 257 | None => LogStreamState::Wait, 258 | some => return Ok(some.into()), 259 | }, 260 | }; 261 | 262 | self.state = next_state; 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /bridge/src/app.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | use tokio_core::reactor::{Handle}; 3 | use tokio_timer::{self, Timer}; 4 | use web3::Transport; 5 | use error::{Error, ResultExt, ErrorKind}; 6 | use config::Config; 7 | use contracts::{home, foreign}; 8 | use web3::transports::http::Http; 9 | use std::time::Duration; 10 | 11 | use std::sync::Arc; 12 | use std::sync::atomic::AtomicBool; 13 | 14 | use ethcore::ethstore::{EthStore,accounts_dir::RootDiskDirectory}; 15 | use ethcore::account_provider::{AccountProvider, AccountProviderSettings}; 16 | 17 | pub struct App where T: Transport { 18 | pub config: Config, 19 | pub database_path: PathBuf, 20 | pub connections: Connections, 21 | pub home_bridge: home::HomeBridge, 22 | pub foreign_bridge: foreign::ForeignBridge, 23 | pub timer: Timer, 24 | pub running: Arc, 25 | pub keystore: AccountProvider, 26 | } 27 | 28 | pub struct Connections where T: Transport { 29 | pub home: T, 30 | pub foreign: T, 31 | } 32 | 33 | impl Connections { 34 | pub fn new_http(handle: &Handle, home: &str, home_concurrent_connections: usize, foreign: &str, foreign_concurrent_connections: usize) -> Result { 35 | 36 | let home = Http::with_event_loop(home, handle,home_concurrent_connections) 37 | .map_err(ErrorKind::Web3) 38 | .map_err(Error::from) 39 | .chain_err(||"Cannot connect to home node rpc")?; 40 | let foreign = Http::with_event_loop(foreign, handle, foreign_concurrent_connections) 41 | .map_err(ErrorKind::Web3) 42 | .map_err(Error::from) 43 | .chain_err(||"Cannot connect to foreign node rpc")?; 44 | 45 | let result = Connections { 46 | home, 47 | foreign 48 | }; 49 | Ok(result) 50 | } 51 | } 52 | 53 | impl Connections { 54 | pub fn as_ref(&self) -> Connections<&T> { 55 | Connections { 56 | home: &self.home, 57 | foreign: &self.foreign, 58 | } 59 | } 60 | } 61 | 62 | impl App { 63 | pub fn new_http>(config: Config, database_path: P, handle: &Handle, running: Arc) -> Result { 64 | let home_url:String = format!("{}:{}", config.home.rpc_host, config.home.rpc_port); 65 | let foreign_url:String = format!("{}:{}", config.foreign.rpc_host, config.foreign.rpc_port); 66 | 67 | let connections = Connections::new_http(handle, home_url.as_ref(), config.home.concurrent_http_requests, foreign_url.as_ref(), config.foreign.concurrent_http_requests)?; 68 | let keystore = EthStore::open(Box::new(RootDiskDirectory::at(&config.keystore))).map_err(|e| ErrorKind::KeyStore(e))?; 69 | 70 | let keystore = AccountProvider::new(Box::new(keystore), AccountProviderSettings { 71 | enable_hardware_wallets: false, 72 | hardware_wallet_classic_key: false, 73 | unlock_keep_secret: true, 74 | blacklisted_accounts: vec![], 75 | }); 76 | keystore.unlock_account_permanently(config.home.account, config.home.password()?).map_err(|e| ErrorKind::AccountError(e))?; 77 | keystore.unlock_account_permanently(config.foreign.account, config.foreign.password()?).map_err(|e| ErrorKind::AccountError(e))?; 78 | 79 | let max_timeout = config.clone().home.request_timeout.max(config.clone().foreign.request_timeout); 80 | 81 | let result = App { 82 | config, 83 | database_path: database_path.as_ref().to_path_buf(), 84 | connections, 85 | home_bridge: home::HomeBridge::default(), 86 | foreign_bridge: foreign::ForeignBridge::default(), 87 | // it is important to build a timer with a max timeout that can accommodate the longest timeout requested, 88 | // otherwise it will result in a bizarrely inadequate behaviour of timing out nearly immediately 89 | timer: tokio_timer::wheel().max_timeout(max_timeout) 90 | .tick_duration(Duration::from_millis(100)) 91 | .num_slots((max_timeout.as_secs() as usize * 10).next_power_of_two()) 92 | .build(), 93 | running, 94 | keystore, 95 | }; 96 | Ok(result) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /bridge/src/bridge/balance.rs: -------------------------------------------------------------------------------- 1 | use futures::{Future, Stream, Poll}; 2 | use tokio_timer::Timeout; 3 | use web3::Transport; 4 | use web3::types::U256; 5 | use api::{self, ApiCall}; 6 | use error::Error; 7 | use config::Node; 8 | use std::sync::Arc; 9 | use app::App; 10 | 11 | /// State of balance checking. 12 | enum BalanceCheckState { 13 | /// Deposit relay is waiting for logs. 14 | Wait, 15 | /// Balance request is in progress. 16 | BalanceRequest { 17 | future: Timeout>, 18 | }, 19 | /// Balance request completed. 20 | Yield(Option), 21 | } 22 | 23 | pub struct BalanceCheck { 24 | app: Arc>, 25 | transport: T, 26 | state: BalanceCheckState, 27 | node: Node, 28 | } 29 | 30 | pub fn create_balance_check(app: Arc>, transport: T, node: Node) -> BalanceCheck { 31 | BalanceCheck { 32 | app, 33 | state: BalanceCheckState::Wait, 34 | transport, 35 | node, 36 | } 37 | } 38 | 39 | impl Stream for BalanceCheck { 40 | type Item = U256; 41 | type Error = Error; 42 | 43 | fn poll(&mut self) -> Poll, Self::Error> { 44 | loop { 45 | let next_state = match self.state { 46 | BalanceCheckState::Wait => { 47 | BalanceCheckState::BalanceRequest { 48 | future: self.app.timer.timeout(api::balance(&self.transport, self.node.account, None), 49 | self.node.request_timeout), 50 | } 51 | }, 52 | BalanceCheckState::BalanceRequest { ref mut future } => { 53 | let value = try_ready!(future.poll()); 54 | BalanceCheckState::Yield(Some(value)) 55 | }, 56 | BalanceCheckState::Yield(ref mut balance) => match balance.take() { 57 | None => BalanceCheckState::Wait, 58 | some => return Ok(some.into()), 59 | } 60 | }; 61 | self.state = next_state; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /bridge/src/bridge/chain_id.rs: -------------------------------------------------------------------------------- 1 | use futures::{Future, Poll}; 2 | use tokio_timer::Timeout; 3 | use web3::Transport; 4 | use api::{self, ApiCall}; 5 | use error::Error; 6 | use config::Node; 7 | use std::sync::Arc; 8 | use app::App; 9 | 10 | /// State of chain id retrieval 11 | enum ChainIdRetrievalState { 12 | /// Chain ID request is waiting to happen 13 | Wait, 14 | /// Request is in progress 15 | ChainIdRequest { 16 | future: Timeout>, 17 | }, 18 | } 19 | 20 | pub struct ChainIdRetrieval { 21 | app: Arc>, 22 | transport: T, 23 | state: ChainIdRetrievalState, 24 | node: Node, 25 | } 26 | 27 | pub fn create_chain_id_retrieval(app: Arc>, transport: T, node: Node) -> ChainIdRetrieval { 28 | ChainIdRetrieval { 29 | app, 30 | state: ChainIdRetrievalState::Wait, 31 | transport, 32 | node, 33 | } 34 | } 35 | 36 | impl Future for ChainIdRetrieval { 37 | type Item = u64; 38 | type Error = Error; 39 | 40 | fn poll(&mut self) -> Poll { 41 | loop { 42 | let next_state = match self.state { 43 | ChainIdRetrievalState::Wait => { 44 | ChainIdRetrievalState::ChainIdRequest { 45 | future: self.app.timer.timeout(api::net_version(&self.transport), 46 | self.node.request_timeout), 47 | } 48 | }, 49 | ChainIdRetrievalState::ChainIdRequest { ref mut future } => { 50 | let value = try_ready!(future.poll()); 51 | let id: u64 = value.parse().unwrap(); 52 | return Ok(id.into()); 53 | }, 54 | }; 55 | self.state = next_state; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /bridge/src/bridge/deploy.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use futures::{Future, Poll}; 3 | #[cfg(feature = "deploy")] 4 | use futures::future; 5 | use web3::Transport; 6 | #[cfg(feature = "deploy")] 7 | use web3::types::U256; 8 | use app::App; 9 | use database::Database; 10 | use error::{Error, ErrorKind}; 11 | #[cfg(feature = "deploy")] 12 | use api; 13 | #[cfg(feature = "deploy")] 14 | use ethcore_transaction::{Transaction, Action}; 15 | #[cfg(feature = "deploy")] 16 | use super::nonce::{NonceCheck,TransactionWithConfirmation}; 17 | 18 | pub enum Deployed { 19 | /// No existing database found. Deployed new contracts. 20 | New(Database), 21 | /// Reusing existing contracts. 22 | Existing(Database), 23 | } 24 | 25 | #[cfg(feature = "deploy")] 26 | enum DeployState { 27 | CheckIfNeeded, 28 | Deploying(future::Join>, NonceCheck>>), 29 | } 30 | 31 | #[cfg(not(feature = "deploy"))] 32 | enum DeployState { 33 | CheckIfNeeded, 34 | } 35 | 36 | #[allow(unused_variables)] 37 | pub fn create_deploy(app: Arc>, home_chain_id: u64, foreign_chain_id: u64) -> Deploy { 38 | Deploy { 39 | app, 40 | state: DeployState::CheckIfNeeded, 41 | #[cfg(feature = "deploy")] 42 | home_chain_id, 43 | #[cfg(feature = "deploy")] 44 | foreign_chain_id, 45 | } 46 | } 47 | 48 | pub struct Deploy { 49 | app: Arc>, 50 | #[cfg(feature = "deploy")] 51 | state: DeployState, 52 | #[cfg(not(feature = "deploy"))] 53 | state: DeployState, 54 | #[cfg(feature = "deploy")] 55 | home_chain_id: u64, 56 | #[cfg(feature = "deploy")] 57 | foreign_chain_id: u64, 58 | } 59 | 60 | impl Future for Deploy { 61 | type Item = Deployed; 62 | type Error = Error; 63 | 64 | fn poll(&mut self) -> Poll { 65 | loop { 66 | let _next_state = match self.state { 67 | DeployState::CheckIfNeeded => match Database::load(&self.app.database_path).map_err(ErrorKind::from) { 68 | Ok(database) => return Ok(Deployed::Existing(database).into()), 69 | Err(ErrorKind::MissingFile(_e)) => { 70 | #[cfg(feature = "deploy")] { 71 | println!("deploy"); 72 | let main_data = self.app.home_bridge.constructor( 73 | self.app.config.home.contract.bin.clone().0, 74 | self.app.config.authorities.required_signatures, 75 | self.app.config.authorities.accounts.clone(), 76 | self.app.config.estimated_gas_cost_of_withdraw 77 | ); 78 | let test_data = self.app.foreign_bridge.constructor( 79 | self.app.config.foreign.contract.bin.clone().0, 80 | self.app.config.authorities.required_signatures, 81 | self.app.config.authorities.accounts.clone(), 82 | self.app.config.estimated_gas_cost_of_withdraw 83 | ); 84 | 85 | let main_tx = Transaction { 86 | nonce: U256::zero(), 87 | gas_price: self.app.config.txs.home_deploy.gas_price.into(), 88 | gas: self.app.config.txs.home_deploy.gas.into(), 89 | action: Action::Create, 90 | value: U256::zero(), 91 | data: main_data.into(), 92 | }; 93 | 94 | let test_tx = Transaction { 95 | nonce: U256::zero(), 96 | gas_price: self.app.config.txs.foreign_deploy.gas_price.into(), 97 | gas: self.app.config.txs.foreign_deploy.gas.into(), 98 | action: Action::Create, 99 | value: U256::zero(), 100 | data: test_data.into(), 101 | }; 102 | 103 | let main_future = api::send_transaction_with_nonce(self.app.connections.home.clone(), self.app.clone(), 104 | self.app.config.home.clone(), main_tx, self.home_chain_id, 105 | TransactionWithConfirmation(self.app.connections.home.clone(), self.app.config.home.poll_interval, self.app.config.home.required_confirmations)); 106 | 107 | let test_future = api::send_transaction_with_nonce(self.app.connections.foreign.clone(), self.app.clone(), 108 | self.app.config.foreign.clone(), test_tx, self.foreign_chain_id, 109 | TransactionWithConfirmation(self.app.connections.foreign.clone(), self.app.config.foreign.poll_interval, self.app.config.foreign.required_confirmations)); 110 | 111 | DeployState::Deploying(main_future.join(test_future)) 112 | } 113 | #[cfg(not(feature = "deploy"))] { 114 | return Err(ErrorKind::MissingFile(_e).into()) 115 | } 116 | }, 117 | Err(err) => return Err(err.into()), 118 | }, 119 | #[cfg(feature = "deploy")] 120 | DeployState::Deploying(ref mut future) => { 121 | let (main_receipt, test_receipt) = try_ready!(future.poll()); 122 | let database = Database { 123 | home_contract_address: main_receipt.contract_address.expect("contract creation receipt must have an address; qed"), 124 | foreign_contract_address: test_receipt.contract_address.expect("contract creation receipt must have an address; qed"), 125 | home_deploy: Some(main_receipt.block_number.low_u64()), 126 | foreign_deploy: Some(test_receipt.block_number.low_u64()), 127 | checked_deposit_relay: main_receipt.block_number.low_u64(), 128 | checked_withdraw_relay: test_receipt.block_number.low_u64(), 129 | checked_withdraw_confirm: test_receipt.block_number.low_u64(), 130 | }; 131 | return Ok(Deployed::New(database).into()) 132 | }, 133 | }; 134 | #[allow(unreachable_code)] { 135 | self.state = _next_state; 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /bridge/src/bridge/deposit_relay.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, RwLock}; 2 | use futures::{self, Future, Stream, stream::{Collect, FuturesUnordered, futures_unordered}, Poll}; 3 | use web3::Transport; 4 | use web3::types::{U256, Address, Bytes, Log, FilterBuilder}; 5 | use ethabi::RawLog; 6 | use api::{LogStream, self}; 7 | use error::{Error, ErrorKind, Result}; 8 | use database::Database; 9 | use contracts::{home, foreign}; 10 | use util::web3_filter; 11 | use app::App; 12 | use ethcore_transaction::{Transaction, Action}; 13 | use super::nonce::{NonceCheck, SendRawTransaction}; 14 | use super::BridgeChecked; 15 | use itertools::Itertools; 16 | 17 | fn deposits_filter(home: &home::HomeBridge, address: Address) -> FilterBuilder { 18 | let filter = home.events().deposit().create_filter(); 19 | web3_filter(filter, ::std::iter::once(address)) 20 | } 21 | 22 | fn deposit_relay_payload(home: &home::HomeBridge, foreign: &foreign::ForeignBridge, log: Log) -> Result { 23 | let raw_log = RawLog { 24 | topics: log.topics, 25 | data: log.data.0, 26 | }; 27 | let deposit_log = home.events().deposit().parse_log(raw_log)?; 28 | let hash = log.transaction_hash.expect("log to be mined and contain `transaction_hash`"); 29 | let payload = foreign.functions().deposit().input(deposit_log.recipient, deposit_log.value, hash.0); 30 | Ok(payload.into()) 31 | } 32 | 33 | /// State of deposits relay. 34 | enum DepositRelayState { 35 | /// Deposit relay is waiting for logs. 36 | Wait, 37 | /// Relaying deposits in progress. 38 | RelayDeposits { 39 | future: Collect>>>, 40 | block: u64, 41 | }, 42 | /// All deposits till given block has been relayed. 43 | Yield(Option), 44 | } 45 | 46 | pub fn create_deposit_relay(app: Arc>, init: &Database, foreign_balance: Arc>>, foreign_chain_id: u64, foreign_gas_price: Arc>) -> DepositRelay { 47 | let logs_init = api::LogStreamInit { 48 | after: init.checked_deposit_relay, 49 | request_timeout: app.config.home.request_timeout, 50 | poll_interval: app.config.home.poll_interval, 51 | confirmations: app.config.home.required_confirmations, 52 | filter: deposits_filter(&app.home_bridge, init.home_contract_address), 53 | }; 54 | DepositRelay { 55 | logs: api::log_stream(app.connections.home.clone(), app.timer.clone(), logs_init), 56 | foreign_contract: init.foreign_contract_address, 57 | state: DepositRelayState::Wait, 58 | app, 59 | foreign_balance, 60 | foreign_chain_id, 61 | foreign_gas_price, 62 | } 63 | } 64 | 65 | pub struct DepositRelay { 66 | app: Arc>, 67 | logs: LogStream, 68 | state: DepositRelayState, 69 | foreign_contract: Address, 70 | foreign_balance: Arc>>, 71 | foreign_chain_id: u64, 72 | foreign_gas_price: Arc>, 73 | } 74 | 75 | impl Stream for DepositRelay { 76 | type Item = BridgeChecked; 77 | type Error = Error; 78 | 79 | fn poll(&mut self) -> Poll, Self::Error> { 80 | loop { 81 | let next_state = match self.state { 82 | DepositRelayState::Wait => { 83 | let foreign_balance = self.foreign_balance.read().unwrap(); 84 | if foreign_balance.is_none() { 85 | warn!("foreign contract balance is unknown"); 86 | return Ok(futures::Async::NotReady); 87 | } 88 | let item = try_stream!(self.logs.poll().map_err(|e| ErrorKind::ContextualizedError(Box::new(e), "polling home for deposits"))); 89 | let len = item.logs.len(); 90 | info!("got {} new deposits to relay", len); 91 | 92 | let gas = U256::from(self.app.config.txs.deposit_relay.gas); 93 | let gas_price = U256::from(*self.foreign_gas_price.read().unwrap()); 94 | let balance_required = gas * gas_price * U256::from(item.logs.len()); 95 | 96 | if balance_required > *foreign_balance.as_ref().unwrap() { 97 | return Err(ErrorKind::InsufficientFunds.into()) 98 | } 99 | let deposits = item.logs 100 | .into_iter() 101 | .map(|log| deposit_relay_payload(&self.app.home_bridge, &self.app.foreign_bridge, log)) 102 | .collect::>>()? 103 | .into_iter() 104 | .map(|payload| { 105 | let tx = Transaction { 106 | gas, 107 | gas_price, 108 | value: U256::zero(), 109 | data: payload.0, 110 | nonce: U256::zero(), 111 | action: Action::Call(self.foreign_contract.clone()), 112 | }; 113 | api::send_transaction_with_nonce(self.app.connections.foreign.clone(), self.app.clone(), self.app.config.foreign.clone(), 114 | tx, self.foreign_chain_id, SendRawTransaction(self.app.connections.foreign.clone())) 115 | }).collect_vec(); 116 | 117 | info!("relaying {} deposits", len); 118 | DepositRelayState::RelayDeposits { 119 | future: futures_unordered(deposits).collect(), 120 | block: item.to, 121 | } 122 | }, 123 | DepositRelayState::RelayDeposits { ref mut future, block } => { 124 | let _ = try_ready!(future.poll().map_err(|e| ErrorKind::ContextualizedError(Box::new(e), "relaying deposit to foreign"))); 125 | info!("deposit relay completed"); 126 | DepositRelayState::Yield(Some(block)) 127 | }, 128 | DepositRelayState::Yield(ref mut block) => match block.take() { 129 | None => DepositRelayState::Wait, 130 | Some(v) => return Ok(Some(BridgeChecked::DepositRelay(v)).into()), 131 | } 132 | }; 133 | self.state = next_state; 134 | } 135 | } 136 | } 137 | 138 | #[cfg(test)] 139 | mod tests { 140 | use rustc_hex::FromHex; 141 | use web3::types::{Log, Bytes, Address}; 142 | use contracts::{home, foreign}; 143 | use super::deposit_relay_payload; 144 | 145 | #[test] 146 | fn test_deposit_relay_payload() { 147 | let home = home::HomeBridge::default(); 148 | let foreign = foreign::ForeignBridge::default(); 149 | 150 | let data = "000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebcccc00000000000000000000000000000000000000000000000000000000000000f0".from_hex().unwrap(); 151 | let log = Log { 152 | data: data.into(), 153 | topics: vec!["e1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c".into()], 154 | transaction_hash: Some("884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a9424364".into()), 155 | address: Address::zero(), 156 | block_hash: None, 157 | transaction_index: None, 158 | log_index: None, 159 | transaction_log_index: None, 160 | log_type: None, 161 | block_number: None, 162 | removed: None, 163 | }; 164 | 165 | let payload = deposit_relay_payload(&home, &foreign, log).unwrap(); 166 | let expected: Bytes = "26b3293f000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebcccc00000000000000000000000000000000000000000000000000000000000000f0884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a9424364".from_hex().unwrap().into(); 167 | assert_eq!(expected, payload); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /bridge/src/bridge/gas_price.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::time::{Duration, Instant}; 3 | 4 | use futures::{Async, Future, Poll, Stream}; 5 | use hyper::{Chunk, client::{HttpConnector, Connect}, Client, Uri, Error as HyperError}; 6 | use hyper_tls::HttpsConnector; 7 | use serde_json as json; 8 | use tokio_core::reactor::Handle; 9 | use tokio_timer::{Interval, Timer, Timeout}; 10 | 11 | use config::{GasPriceSpeed, Node}; 12 | use error::Error; 13 | 14 | const CACHE_TIMEOUT_DURATION: Duration = Duration::from_secs(5 * 60); 15 | 16 | enum State { 17 | Initial, 18 | WaitingForResponse(Timeout), 19 | Yield(Option), 20 | } 21 | 22 | pub trait Retriever { 23 | type Item: AsRef<[u8]>; 24 | type Future: Future; 25 | fn retrieve(&self, uri: &Uri) -> Self::Future; 26 | } 27 | 28 | impl Retriever for Client where C: Connect, B: Stream + 'static { 29 | type Item = Chunk; 30 | type Future = Box>; 31 | 32 | fn retrieve(&self, uri: &Uri) -> Self::Future { 33 | Box::new( 34 | self.get(uri.clone()) 35 | .and_then(|resp| resp.body().concat2()) 36 | .map_err(|e| e.into()) 37 | ) 38 | } 39 | } 40 | 41 | pub type StandardGasPriceStream = GasPriceStream>, Client>, Chunk>; 42 | 43 | pub struct GasPriceStream where I: AsRef<[u8]>, F: Future, R: Retriever { 44 | state: State, 45 | retriever: R, 46 | uri: Uri, 47 | speed: GasPriceSpeed, 48 | request_timer: Timer, 49 | interval: Interval, 50 | last_price: u64, 51 | request_timeout: Duration, 52 | } 53 | 54 | impl StandardGasPriceStream { 55 | pub fn new(node: &Node, handle: &Handle, timer: &Timer) -> Self { 56 | let client = Client::configure() 57 | .connector(HttpsConnector::new(4, handle).unwrap()) 58 | .build(handle); 59 | GasPriceStream::new_with_retriever(node, client, timer) 60 | } 61 | } 62 | 63 | impl GasPriceStream where I: AsRef<[u8]>, F: Future, R: Retriever { 64 | pub fn new_with_retriever(node: &Node, retriever: R, timer: &Timer) -> Self { 65 | let uri: Uri = node.gas_price_oracle_url.clone().unwrap().parse().unwrap(); 66 | 67 | GasPriceStream { 68 | state: State::Initial, 69 | retriever, 70 | uri, 71 | speed: node.gas_price_speed, 72 | request_timer: timer.clone(), 73 | interval: timer.interval_at(Instant::now(), CACHE_TIMEOUT_DURATION), 74 | last_price: node.default_gas_price, 75 | request_timeout: node.gas_price_timeout, 76 | } 77 | } 78 | } 79 | 80 | impl Stream for GasPriceStream where I: AsRef<[u8]>, F: Future, R: Retriever { 81 | type Item = u64; 82 | type Error = Error; 83 | 84 | fn poll(&mut self) -> Poll, Self::Error> { 85 | loop { 86 | let next_state = match self.state { 87 | State::Initial => { 88 | let _ = try_stream!(self.interval.poll()); 89 | 90 | let request = self.retriever.retrieve(&self.uri); 91 | 92 | let request_future = self.request_timer 93 | .timeout(request, self.request_timeout); 94 | 95 | State::WaitingForResponse(request_future) 96 | }, 97 | State::WaitingForResponse(ref mut request_future) => { 98 | match request_future.poll() { 99 | Ok(Async::NotReady) => return Ok(Async::NotReady), 100 | Ok(Async::Ready(chunk)) => { 101 | match json::from_slice::>(chunk.as_ref()) { 102 | Ok(json_obj) => { 103 | match json_obj.get(self.speed.as_str()) { 104 | Some(json::Value::Number(price)) => State::Yield(Some((price.as_f64().unwrap() * 1_000_000_000.0).trunc() as u64)), 105 | _ => { 106 | error!("Invalid or missing gas price ({}) in the gas price oracle response: {}", self.speed.as_str(), String::from_utf8_lossy(&*chunk.as_ref())); 107 | State::Yield(Some(self.last_price)) 108 | }, 109 | } 110 | }, 111 | Err(e) => { 112 | error!("Error while parsing response from gas price oracle: {:?} {}", e, String::from_utf8_lossy(&*chunk.as_ref())); 113 | State::Yield(Some(self.last_price)) 114 | } 115 | } 116 | }, 117 | Err(e) => { 118 | error!("Error while fetching gas price: {:?}", e); 119 | State::Yield(Some(self.last_price)) 120 | }, 121 | } 122 | }, 123 | State::Yield(ref mut opt) => match opt.take() { 124 | None => State::Initial, 125 | Some(price) => { 126 | if price != self.last_price { 127 | info!("Gas price: {} gwei", (price as f64) / 1_000_000_000.0); 128 | self.last_price = price; 129 | } 130 | return Ok(Async::Ready(Some(price))) 131 | }, 132 | } 133 | }; 134 | 135 | self.state = next_state; 136 | } 137 | } 138 | } 139 | 140 | #[cfg(test)] 141 | mod tests { 142 | 143 | use super::*; 144 | use error::{Error, ErrorKind}; 145 | use futures::{Async, future::{err, ok, FutureResult}}; 146 | use config::{Node, NodeInfo, DEFAULT_CONCURRENCY}; 147 | use tokio_timer::Timer; 148 | use std::time::Duration; 149 | use std::path::PathBuf; 150 | use web3::types::Address; 151 | use std::str::FromStr; 152 | 153 | struct ErroredRequest; 154 | 155 | impl Retriever for ErroredRequest { 156 | type Item = Vec; 157 | type Future = FutureResult; 158 | 159 | fn retrieve(&self, _uri: &Uri) -> ::Future { 160 | err(ErrorKind::OtherError("something went wrong".into()).into()) 161 | } 162 | } 163 | 164 | #[test] 165 | fn errored_request() { 166 | let node = Node { 167 | account: Address::new(), 168 | request_timeout: Duration::from_secs(5), 169 | poll_interval: Duration::from_secs(1), 170 | required_confirmations: 0, 171 | rpc_host: "https://rpc".into(), 172 | rpc_port: 443, 173 | password: PathBuf::from("password"), 174 | info: NodeInfo::default(), 175 | gas_price_oracle_url: Some("https://gas.price".into()), 176 | gas_price_speed: GasPriceSpeed::from_str("fast").unwrap(), 177 | gas_price_timeout: Duration::from_secs(5), 178 | default_gas_price: 15_000_000_000, 179 | concurrent_http_requests: DEFAULT_CONCURRENCY, 180 | }; 181 | let timer = Timer::default(); 182 | let mut stream = GasPriceStream::new_with_retriever(&node, ErroredRequest, &timer); 183 | loop { 184 | match stream.poll() { 185 | Ok(Async::Ready(Some(v))) => { 186 | assert_eq!(v, node.default_gas_price); 187 | break; 188 | }, 189 | Err(_) => panic!("should not error out"), 190 | _ => (), 191 | } 192 | } 193 | } 194 | 195 | 196 | struct BadJson; 197 | 198 | impl Retriever for BadJson { 199 | type Item = String; 200 | type Future = FutureResult; 201 | 202 | fn retrieve(&self, _uri: &Uri) -> ::Future { 203 | ok("bad json".into()) 204 | } 205 | } 206 | 207 | #[test] 208 | fn bad_json() { 209 | let node = Node { 210 | account: Address::new(), 211 | request_timeout: Duration::from_secs(5), 212 | poll_interval: Duration::from_secs(1), 213 | required_confirmations: 0, 214 | rpc_host: "https://rpc".into(), 215 | rpc_port: 443, 216 | password: PathBuf::from("password"), 217 | info: NodeInfo::default(), 218 | gas_price_oracle_url: Some("https://gas.price".into()), 219 | gas_price_speed: GasPriceSpeed::from_str("fast").unwrap(), 220 | gas_price_timeout: Duration::from_secs(5), 221 | default_gas_price: 15_000_000_000, 222 | concurrent_http_requests: DEFAULT_CONCURRENCY, 223 | }; 224 | let timer = Timer::default(); 225 | let mut stream = GasPriceStream::new_with_retriever(&node, BadJson, &timer); 226 | loop { 227 | match stream.poll() { 228 | Ok(Async::Ready(Some(v))) => { 229 | assert_eq!(v, node.default_gas_price); 230 | break; 231 | }, 232 | Err(_) => panic!("should not error out"), 233 | _ => (), 234 | } 235 | } 236 | } 237 | 238 | 239 | struct UnexpectedJson; 240 | 241 | impl Retriever for UnexpectedJson { 242 | type Item = String; 243 | type Future = FutureResult; 244 | 245 | fn retrieve(&self, _uri: &Uri) -> ::Future { 246 | ok(r#"{"cow": "moo"}"#.into()) 247 | } 248 | } 249 | 250 | #[test] 251 | fn unexpected_json() { 252 | let node = Node { 253 | account: Address::new(), 254 | request_timeout: Duration::from_secs(5), 255 | poll_interval: Duration::from_secs(1), 256 | required_confirmations: 0, 257 | rpc_host: "https://rpc".into(), 258 | rpc_port: 443, 259 | password: PathBuf::from("password"), 260 | info: NodeInfo::default(), 261 | gas_price_oracle_url: Some("https://gas.price".into()), 262 | gas_price_speed: GasPriceSpeed::from_str("fast").unwrap(), 263 | gas_price_timeout: Duration::from_secs(5), 264 | default_gas_price: 15_000_000_000, 265 | concurrent_http_requests: DEFAULT_CONCURRENCY, 266 | }; 267 | let timer = Timer::default(); 268 | let mut stream = GasPriceStream::new_with_retriever(&node, UnexpectedJson, &timer); 269 | loop { 270 | match stream.poll() { 271 | Ok(Async::Ready(Some(v))) => { 272 | assert_eq!(v, node.default_gas_price); 273 | break; 274 | }, 275 | Err(_) => panic!("should not error out"), 276 | _ => (), 277 | } 278 | } 279 | } 280 | 281 | struct NonObjectJson; 282 | 283 | impl Retriever for NonObjectJson { 284 | type Item = String; 285 | type Future = FutureResult; 286 | 287 | fn retrieve(&self, _uri: &Uri) -> ::Future { 288 | ok("3".into()) 289 | } 290 | } 291 | 292 | #[test] 293 | fn non_object_json() { 294 | let node = Node { 295 | account: Address::new(), 296 | request_timeout: Duration::from_secs(5), 297 | poll_interval: Duration::from_secs(1), 298 | required_confirmations: 0, 299 | rpc_host: "https://rpc".into(), 300 | rpc_port: 443, 301 | password: PathBuf::from("password"), 302 | info: NodeInfo::default(), 303 | gas_price_oracle_url: Some("https://gas.price".into()), 304 | gas_price_speed: GasPriceSpeed::from_str("fast").unwrap(), 305 | gas_price_timeout: Duration::from_secs(5), 306 | default_gas_price: 15_000_000_000, 307 | concurrent_http_requests: DEFAULT_CONCURRENCY, 308 | }; 309 | let timer = Timer::default(); 310 | let mut stream = GasPriceStream::new_with_retriever(&node, NonObjectJson, &timer); 311 | loop { 312 | match stream.poll() { 313 | Ok(Async::Ready(Some(v))) => { 314 | assert_eq!(v, node.default_gas_price); 315 | break; 316 | }, 317 | Err(_) => panic!("should not error out"), 318 | _ => (), 319 | } 320 | } 321 | } 322 | 323 | struct CorrectJson; 324 | 325 | impl Retriever for CorrectJson { 326 | type Item = String; 327 | type Future = FutureResult; 328 | 329 | fn retrieve(&self, _uri: &Uri) -> ::Future { 330 | ok(r#"{"fast": 12.0}"#.into()) 331 | } 332 | } 333 | 334 | #[test] 335 | fn correct_json() { 336 | let node = Node { 337 | account: Address::new(), 338 | request_timeout: Duration::from_secs(5), 339 | poll_interval: Duration::from_secs(1), 340 | required_confirmations: 0, 341 | rpc_host: "https://rpc".into(), 342 | rpc_port: 443, 343 | password: PathBuf::from("password"), 344 | info: NodeInfo::default(), 345 | gas_price_oracle_url: Some("https://gas.price".into()), 346 | gas_price_speed: GasPriceSpeed::from_str("fast").unwrap(), 347 | gas_price_timeout: Duration::from_secs(5), 348 | default_gas_price: 15_000_000_000, 349 | concurrent_http_requests: DEFAULT_CONCURRENCY, 350 | }; 351 | let timer = Timer::default(); 352 | let mut stream = GasPriceStream::new_with_retriever(&node, CorrectJson, &timer); 353 | loop { 354 | match stream.poll() { 355 | Ok(Async::Ready(Some(v))) => { 356 | assert_eq!(v, 12_000_000_000); 357 | break; 358 | }, 359 | Err(_) => panic!("should not error out"), 360 | _ => (), 361 | } 362 | } 363 | } 364 | 365 | } 366 | 367 | -------------------------------------------------------------------------------- /bridge/src/bridge/mod.rs: -------------------------------------------------------------------------------- 1 | mod deploy; 2 | mod balance; 3 | mod chain_id; 4 | pub mod nonce; 5 | mod deposit_relay; 6 | mod withdraw_confirm; 7 | mod withdraw_relay; 8 | mod gas_price; 9 | 10 | use std::fs; 11 | use std::sync::{Arc, RwLock}; 12 | use std::path::PathBuf; 13 | use futures::{Stream, Poll, Async}; 14 | use web3::Transport; 15 | use web3::types::U256; 16 | use app::App; 17 | use database::Database; 18 | use error::{Error, ErrorKind}; 19 | use tokio_core::reactor::Handle; 20 | 21 | pub use self::deploy::{Deploy, Deployed, create_deploy}; 22 | pub use self::balance::{BalanceCheck, create_balance_check}; 23 | pub use self::chain_id::{ChainIdRetrieval, create_chain_id_retrieval}; 24 | pub use self::deposit_relay::{DepositRelay, create_deposit_relay}; 25 | pub use self::withdraw_relay::{WithdrawRelay, create_withdraw_relay}; 26 | pub use self::withdraw_confirm::{WithdrawConfirm, create_withdraw_confirm}; 27 | pub use self::gas_price::StandardGasPriceStream; 28 | 29 | /// Last block checked by the bridge components. 30 | #[derive(Clone, Copy)] 31 | pub enum BridgeChecked { 32 | DepositRelay(u64), 33 | WithdrawRelay(u64), 34 | WithdrawConfirm(u64), 35 | } 36 | 37 | pub struct Bridge> { 38 | path: PathBuf, 39 | database: Database, 40 | event_stream: ES, 41 | } 42 | 43 | impl> Stream for Bridge { 44 | type Item = (); 45 | type Error = Error; 46 | 47 | fn poll(&mut self) -> Poll, Self::Error> { 48 | let check = try_stream!(self.event_stream.poll()); 49 | match check { 50 | BridgeChecked::DepositRelay(n) => { 51 | self.database.checked_deposit_relay = n; 52 | }, 53 | BridgeChecked::WithdrawRelay(n) => { 54 | self.database.checked_withdraw_relay = n; 55 | }, 56 | BridgeChecked::WithdrawConfirm(n) => { 57 | self.database.checked_withdraw_confirm = n; 58 | }, 59 | } 60 | let file = fs::OpenOptions::new() 61 | .write(true) 62 | .create(true) 63 | .open(&self.path)?; 64 | 65 | self.database.save(file)?; 66 | Ok(Async::Ready(Some(()))) 67 | } 68 | } 69 | 70 | 71 | /// Creates new bridge. 72 | pub fn create_bridge<'a, T: Transport + 'a + Clone>(app: Arc>, init: &Database, handle: &Handle, home_chain_id: u64, foreign_chain_id: u64) -> Bridge> { 73 | Bridge { 74 | path: app.database_path.clone(), 75 | database: init.clone(), 76 | event_stream: create_bridge_event_stream(app, init, handle, home_chain_id, foreign_chain_id), 77 | } 78 | } 79 | 80 | /// Creates new bridge writing to custom backend. 81 | pub fn create_bridge_event_stream<'a, T: Transport + 'a + Clone>(app: Arc>, init: &Database, handle: &Handle, home_chain_id: u64, foreign_chain_id: u64) -> BridgeEventStream<'a, T> { 82 | let home_balance = Arc::new(RwLock::new(None)); 83 | let foreign_balance = Arc::new(RwLock::new(None)); 84 | 85 | let home_gas_stream = if app.config.home.gas_price_oracle_url.is_some() { 86 | let stream = StandardGasPriceStream::new(&app.config.home, handle, &app.timer); 87 | Some(stream) 88 | } else { 89 | None 90 | }; 91 | 92 | let foreign_gas_stream = if app.config.foreign.gas_price_oracle_url.is_some() { 93 | let stream = StandardGasPriceStream::new(&app.config.foreign, handle, &app.timer); 94 | Some(stream) 95 | } else { 96 | None 97 | }; 98 | 99 | let home_gas_price = Arc::new(RwLock::new(app.config.home.default_gas_price)); 100 | let foreign_gas_price = Arc::new(RwLock::new(app.config.foreign.default_gas_price)); 101 | 102 | let deposit_relay = create_deposit_relay(app.clone(), init, foreign_balance.clone(), foreign_chain_id, foreign_gas_price.clone()) 103 | .map_err(|e| ErrorKind::ContextualizedError(Box::new(e), "deposit_relay").into()); 104 | let withdraw_relay = create_withdraw_relay(app.clone(), init, home_balance.clone(), home_chain_id, home_gas_price.clone()) 105 | .map_err(|e| ErrorKind::ContextualizedError(Box::new(e), "withdraw_relay").into()); 106 | let withdraw_confirm = create_withdraw_confirm(app.clone(), init, foreign_balance.clone(), foreign_chain_id, foreign_gas_price.clone()) 107 | .map_err(|e| ErrorKind::ContextualizedError(Box::new(e), "withdraw_confirm").into()); 108 | 109 | let bridge = Box::new(deposit_relay.select(withdraw_relay).select(withdraw_confirm)); 110 | 111 | BridgeEventStream { 112 | foreign_balance_check: create_balance_check(app.clone(), app.connections.foreign.clone(), app.config.foreign.clone()), 113 | home_balance_check: create_balance_check(app.clone(), app.connections.home.clone(), app.config.home.clone()), 114 | foreign_balance: foreign_balance.clone(), 115 | home_balance: home_balance.clone(), 116 | bridge, 117 | state: BridgeStatus::Init, 118 | running: app.running.clone(), 119 | home_gas_stream, 120 | foreign_gas_stream, 121 | home_gas_price, 122 | foreign_gas_price, 123 | } 124 | } 125 | 126 | enum BridgeStatus { 127 | Init, 128 | Wait, 129 | NextItem(Option), 130 | } 131 | 132 | pub struct BridgeEventStream<'a, T: Transport + 'a> { 133 | home_balance_check: BalanceCheck, 134 | foreign_balance_check: BalanceCheck, 135 | home_balance: Arc>>, 136 | foreign_balance: Arc>>, 137 | bridge: Box + 'a>, 138 | state: BridgeStatus, 139 | running: Arc, 140 | home_gas_stream: Option, 141 | foreign_gas_stream: Option, 142 | home_gas_price: Arc>, 143 | foreign_gas_price: Arc>, 144 | } 145 | 146 | use std::sync::atomic::{AtomicBool, Ordering}; 147 | 148 | impl<'a, T: Transport + 'a> BridgeEventStream<'a, T> { 149 | fn check_balances(&mut self) -> Poll, Error> { 150 | let mut home_balance = self.home_balance.write().unwrap(); 151 | let mut foreign_balance = self.foreign_balance.write().unwrap(); 152 | let home_balance_known = home_balance.is_some(); 153 | let foreign_balance_known = foreign_balance.is_some(); 154 | *home_balance = try_bridge!(self.home_balance_check.poll()).or(*home_balance); 155 | *foreign_balance = try_bridge!(self.foreign_balance_check.poll()).or(*foreign_balance); 156 | if !home_balance_known && home_balance.is_some() { 157 | info!("Retrieved home contract balance"); 158 | } 159 | if !foreign_balance_known && foreign_balance.is_some() { 160 | info!("Retrieved foreign contract balance"); 161 | } 162 | if home_balance.is_none() || foreign_balance.is_none() { 163 | Ok(Async::NotReady) 164 | } else { 165 | Ok(Async::Ready(None)) 166 | } 167 | } 168 | 169 | fn get_gas_prices(&mut self) -> Poll, Error> { 170 | if let Some(ref mut home_gas_stream) = self.home_gas_stream { 171 | let mut home_price = self.home_gas_price.write().unwrap(); 172 | *home_price = try_bridge!(home_gas_stream.poll()).unwrap_or(*home_price); 173 | } 174 | 175 | if let Some(ref mut foreign_gas_stream) = self.foreign_gas_stream { 176 | let mut foreign_price = self.foreign_gas_price.write().unwrap(); 177 | *foreign_price = try_bridge!(foreign_gas_stream.poll()).unwrap_or(*foreign_price); 178 | } 179 | 180 | Ok(Async::Ready(None)) 181 | } 182 | } 183 | 184 | impl<'a, T: Transport + 'a> Stream for BridgeEventStream<'a, T> { 185 | type Item = BridgeChecked; 186 | type Error = Error; 187 | 188 | fn poll(&mut self) -> Poll, Self::Error> { 189 | loop { 190 | let next_state = match self.state { 191 | BridgeStatus::Init => { 192 | match self.check_balances()? { 193 | Async::NotReady => return Ok(Async::NotReady), 194 | _ => (), 195 | } 196 | BridgeStatus::Wait 197 | }, 198 | BridgeStatus::Wait => { 199 | if !self.running.load(Ordering::SeqCst) { 200 | return Err(ErrorKind::ShutdownRequested.into()) 201 | } 202 | 203 | let _ = self.get_gas_prices(); 204 | 205 | let item = try_stream!(self.bridge.poll()); 206 | BridgeStatus::NextItem(Some(item)) 207 | }, 208 | BridgeStatus::NextItem(ref mut v) => match v.take() { 209 | None => BridgeStatus::Init, 210 | some => { 211 | return Ok(some.into()); 212 | } 213 | } 214 | }; 215 | 216 | self.state = next_state; 217 | } 218 | } 219 | } 220 | 221 | #[cfg(test)] 222 | mod tests { 223 | extern crate tempdir; 224 | use self::tempdir::TempDir; 225 | use database::Database; 226 | use super::{Bridge, BridgeChecked}; 227 | use error::Error; 228 | use tokio_core::reactor::Core; 229 | use futures::{Stream, stream}; 230 | 231 | #[test] 232 | fn test_database_updates() { 233 | let tempdir = TempDir::new("test_file_backend").unwrap(); 234 | let mut path = tempdir.path().to_owned(); 235 | path.push("db"); 236 | 237 | let bridge = Bridge { 238 | path: path.clone(), 239 | database: Database::default(), 240 | event_stream: stream::iter_ok::<_, Error>(vec![BridgeChecked::DepositRelay(1)]), 241 | }; 242 | 243 | let mut event_loop = Core::new().unwrap(); 244 | let _ = event_loop.run(bridge.collect()); 245 | 246 | let db = Database::load(&path).unwrap(); 247 | assert_eq!(1, db.checked_deposit_relay); 248 | assert_eq!(0, db.checked_withdraw_confirm); 249 | assert_eq!(0, db.checked_withdraw_relay); 250 | 251 | let bridge = Bridge { 252 | path: path.clone(), 253 | database: Database::default(), 254 | event_stream: stream::iter_ok::<_, Error>(vec![BridgeChecked::DepositRelay(2), BridgeChecked::WithdrawConfirm(3), BridgeChecked::WithdrawRelay(2)]), 255 | }; 256 | 257 | let mut event_loop = Core::new().unwrap(); 258 | let _ = event_loop.run(bridge.collect()); 259 | 260 | let db = Database::load(&path).unwrap(); 261 | assert_eq!(2, db.checked_deposit_relay); 262 | assert_eq!(3, db.checked_withdraw_confirm); 263 | assert_eq!(2, db.checked_withdraw_relay); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /bridge/src/bridge/nonce.rs: -------------------------------------------------------------------------------- 1 | use futures::{Future, Async, Poll, future::{MapErr}}; 2 | use tokio_timer::Timeout; 3 | use web3::{self, Transport}; 4 | use web3::types::{U256, H256, Bytes}; 5 | use ethcore_transaction::Transaction; 6 | use api::{self, ApiCall}; 7 | use error::{Error, ErrorKind}; 8 | use config::Node; 9 | use transaction::prepare_raw_transaction; 10 | use app::App; 11 | use std::sync::Arc; 12 | use rpc; 13 | 14 | /// State of balance checking. 15 | enum NonceCheckState { 16 | /// Ready 17 | Ready, 18 | /// Ready to request nonce 19 | Reacquire, 20 | /// Nonce request is in progress. 21 | NonceRequest { 22 | future: Timeout>, 23 | }, 24 | /// Nonce available 25 | Nonce(U256), 26 | /// Transaction is in progress 27 | TransactionRequest { 28 | future: Timeout, 29 | }, 30 | } 31 | 32 | pub struct NonceCheck { 33 | app: Arc>, 34 | transport: T, 35 | state: NonceCheckState, 36 | node: Node, 37 | transaction: Transaction, 38 | chain_id: u64, 39 | sender: S, 40 | } 41 | 42 | use std::fmt::{self, Debug}; 43 | 44 | impl Debug for NonceCheck { 45 | fn fmt(&self, _fmt: &mut fmt::Formatter) -> fmt::Result { 46 | Ok(()) 47 | } 48 | 49 | } 50 | 51 | pub fn send_transaction_with_nonce(transport: T, app: Arc>, node: Node, transaction: Transaction, chain_id: u64, sender: S) -> NonceCheck { 52 | NonceCheck { 53 | app, 54 | state: NonceCheckState::Ready, 55 | transport, 56 | node, 57 | transaction, 58 | chain_id, 59 | sender, 60 | } 61 | } 62 | 63 | impl Future for NonceCheck { 64 | type Item = S::T; 65 | type Error = Error; 66 | 67 | fn poll(&mut self) -> Poll { 68 | loop { 69 | let next_state = match self.state { 70 | NonceCheckState::Ready => { 71 | let result = NonceCheckState::Nonce(self.node.info.nonce.read().unwrap().clone()); 72 | { 73 | let mut node_nonce = self.node.info.nonce.write().unwrap(); 74 | let (next, _) = node_nonce.overflowing_add(U256::one()); 75 | *node_nonce = next; 76 | } 77 | result 78 | }, 79 | NonceCheckState::Reacquire => { 80 | NonceCheckState::NonceRequest { 81 | future: self.app.timer.timeout(api::eth_get_transaction_count(&self.transport, self.node.account, None), 82 | self.node.request_timeout), 83 | } 84 | }, 85 | NonceCheckState::NonceRequest { ref mut future } => { 86 | let nonce = try_ready!(future.poll()); 87 | let mut node_nonce = self.node.info.nonce.write().unwrap(); 88 | *node_nonce = nonce; 89 | NonceCheckState::Nonce(nonce) 90 | }, 91 | NonceCheckState::Nonce(mut nonce) => { 92 | self.transaction.nonce = nonce; 93 | match prepare_raw_transaction(self.transaction.clone(), &self.app, &self.node, self.chain_id) { 94 | Ok(tx) => NonceCheckState::TransactionRequest { 95 | future: self.app.timer.timeout(self.sender.send(tx), self.node.request_timeout) 96 | }, 97 | Err(e) => return Err(e), 98 | } 99 | }, 100 | NonceCheckState::TransactionRequest { ref mut future } => { 101 | match future.poll() { 102 | Ok(Async::Ready(t)) => return Ok(Async::Ready(t)), 103 | Ok(Async::NotReady) => return Ok(Async::NotReady), 104 | Err(e) => match e { 105 | Error(ErrorKind::Web3(web3::error::Error(web3::error::ErrorKind::Rpc(rpc_err), _)), _) => { 106 | if rpc_err.code == rpc::ErrorCode::ServerError(-32010) && rpc_err.message.ends_with("incrementing the nonce.") { 107 | // restart the process 108 | NonceCheckState::Reacquire 109 | } else if rpc_err.code == rpc::ErrorCode::ServerError(-32010) && rpc_err.message.ends_with("already imported.") { 110 | let hash = self.transaction.hash(Some(self.chain_id)); 111 | info!("{} already imported on {}, skipping", hash, self.node.rpc_host); 112 | return Ok(Async::Ready(self.sender.ignore(hash))) 113 | } else { 114 | return Err(ErrorKind::Web3(web3::error::ErrorKind::Rpc(rpc_err).into()).into()); 115 | } 116 | }, 117 | e => return Err(From::from(e)), 118 | }, 119 | } 120 | }, 121 | }; 122 | self.state = next_state; 123 | } 124 | } 125 | } 126 | 127 | pub trait TransactionSender { 128 | type T; 129 | type Future : Future; 130 | fn send(&self, tx: Bytes) -> Self::Future; 131 | fn ignore(&self, hash: H256) -> Self::T; 132 | } 133 | 134 | pub struct SendRawTransaction(pub T); 135 | 136 | impl TransactionSender for SendRawTransaction { 137 | type T = H256; 138 | type Future = ApiCall; 139 | 140 | fn send(&self, tx: Bytes) -> ::Future { 141 | api::send_raw_transaction(self.0.clone(), tx) 142 | } 143 | 144 | fn ignore(&self, hash: H256) -> Self::T { 145 | hash 146 | } 147 | 148 | } 149 | 150 | use std::time::Duration; 151 | pub struct TransactionWithConfirmation(pub T, pub Duration, pub usize); 152 | 153 | use web3::types::TransactionReceipt; 154 | 155 | impl TransactionSender for TransactionWithConfirmation { 156 | type T = TransactionReceipt; 157 | type Future = MapErr, fn(::web3::Error) -> Error>; 158 | 159 | fn send(&self, tx: Bytes) -> ::Future { 160 | api::send_raw_transaction_with_confirmation(self.0.clone(), tx, self.1, self.2) 161 | .map_err( web3_error_to_error) 162 | } 163 | 164 | fn ignore(&self, hash: H256) -> Self::T { 165 | let mut receipt = TransactionReceipt::default(); 166 | receipt.transaction_hash = hash; 167 | receipt 168 | } 169 | 170 | } 171 | 172 | fn web3_error_to_error(err: web3::Error) -> Error { 173 | ErrorKind::Web3(err).into() 174 | } 175 | -------------------------------------------------------------------------------- /bridge/src/bridge/withdraw_confirm.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, RwLock}; 2 | use std::ops; 3 | use futures::{self, Future, Stream, stream::{Collect, FuturesUnordered, futures_unordered}, Poll}; 4 | use web3::Transport; 5 | use web3::types::{U256, H520, Address, Bytes, FilterBuilder}; 6 | use api::{self, LogStream}; 7 | use app::App; 8 | use contracts::foreign; 9 | use util::web3_filter; 10 | use database::Database; 11 | use error::{Error, ErrorKind}; 12 | use message_to_mainnet::{MessageToMainnet, MESSAGE_LENGTH}; 13 | use ethcore_transaction::{Transaction, Action}; 14 | use itertools::Itertools; 15 | use super::nonce::{NonceCheck, SendRawTransaction}; 16 | use super::BridgeChecked; 17 | 18 | fn withdraws_filter(foreign: &foreign::ForeignBridge, address: Address) -> FilterBuilder { 19 | let filter = foreign.events().withdraw().create_filter(); 20 | web3_filter(filter, ::std::iter::once(address)) 21 | } 22 | 23 | fn withdraw_submit_signature_payload(foreign: &foreign::ForeignBridge, withdraw_message: Vec, signature: H520) -> Bytes { 24 | assert_eq!(withdraw_message.len(), MESSAGE_LENGTH, "ForeignBridge never accepts messages with len != {} bytes; qed", MESSAGE_LENGTH); 25 | foreign.functions().submit_signature().input(signature.0.to_vec(), withdraw_message).into() 26 | } 27 | 28 | /// State of withdraw confirmation. 29 | enum WithdrawConfirmState { 30 | /// Withdraw confirm is waiting for logs. 31 | Wait, 32 | /// Confirming withdraws. 33 | ConfirmWithdraws { 34 | future: Collect>>>, 35 | block: u64, 36 | }, 37 | /// All withdraws till given block has been confirmed. 38 | Yield(Option), 39 | } 40 | 41 | pub fn create_withdraw_confirm(app: Arc>, init: &Database, foreign_balance: Arc>>, foreign_chain_id: u64, foreign_gas_price: Arc>) -> WithdrawConfirm { 42 | let logs_init = api::LogStreamInit { 43 | after: init.checked_withdraw_confirm, 44 | request_timeout: app.config.foreign.request_timeout, 45 | poll_interval: app.config.foreign.poll_interval, 46 | confirmations: app.config.foreign.required_confirmations, 47 | filter: withdraws_filter(&app.foreign_bridge, init.foreign_contract_address.clone()), 48 | }; 49 | 50 | WithdrawConfirm { 51 | logs: api::log_stream(app.connections.foreign.clone(), app.timer.clone(), logs_init), 52 | foreign_contract: init.foreign_contract_address, 53 | state: WithdrawConfirmState::Wait, 54 | app, 55 | foreign_balance, 56 | foreign_chain_id, 57 | foreign_gas_price, 58 | } 59 | } 60 | 61 | pub struct WithdrawConfirm { 62 | app: Arc>, 63 | logs: LogStream, 64 | state: WithdrawConfirmState, 65 | foreign_contract: Address, 66 | foreign_balance: Arc>>, 67 | foreign_chain_id: u64, 68 | foreign_gas_price: Arc>, 69 | } 70 | 71 | impl Stream for WithdrawConfirm { 72 | type Item = BridgeChecked; 73 | type Error = Error; 74 | 75 | fn poll(&mut self) -> Poll, Self::Error> { 76 | // borrow checker... 77 | let app = &self.app; 78 | let gas = self.app.config.txs.withdraw_confirm.gas.into(); 79 | let gas_price = U256::from(*self.foreign_gas_price.read().unwrap()); 80 | let contract = self.foreign_contract.clone(); 81 | loop { 82 | let next_state = match self.state { 83 | WithdrawConfirmState::Wait => { 84 | let foreign_balance = self.foreign_balance.read().unwrap(); 85 | if foreign_balance.is_none() { 86 | warn!("foreign contract balance is unknown"); 87 | return Ok(futures::Async::NotReady); 88 | } 89 | 90 | let item = try_stream!(self.logs.poll().map_err(|e| ErrorKind::ContextualizedError(Box::new(e), "polling foreign for withdrawals"))); 91 | let len = item.logs.len(); 92 | info!("got {} new withdraws to sign", len); 93 | let mut messages = item.logs 94 | .into_iter() 95 | .map(|log| { 96 | info!("withdraw is ready for signature submission. tx hash {}", log.transaction_hash.unwrap()); 97 | Ok(MessageToMainnet::from_log(log)?.to_bytes()) 98 | }) 99 | .collect::, Error>>()?; 100 | 101 | info!("signing"); 102 | 103 | let signatures = messages.clone() 104 | .into_iter() 105 | .map(|message| 106 | app.keystore.sign(self.app.config.foreign.account, None, api::eth_data_hash(message))) 107 | .map_results(|sig| H520::from(sig.into_electrum())) 108 | .fold_results(vec![], |mut acc, sig| { 109 | acc.push(sig); 110 | acc 111 | }) 112 | .map_err(|e| ErrorKind::SignError(e))?; 113 | 114 | let block = item.to; 115 | 116 | let balance_required = gas * gas_price * U256::from(signatures.len()); 117 | if balance_required > *foreign_balance.as_ref().unwrap() { 118 | return Err(ErrorKind::InsufficientFunds.into()) 119 | } 120 | 121 | info!("signing complete"); 122 | let confirmations = messages 123 | .drain(ops::RangeFull) 124 | .zip(signatures.into_iter()) 125 | .map(|(withdraw_message, signature)| { 126 | withdraw_submit_signature_payload(&app.foreign_bridge, withdraw_message, signature) 127 | }) 128 | .map(|payload| { 129 | let tx = Transaction { 130 | gas, 131 | gas_price, 132 | value: U256::zero(), 133 | data: payload.0, 134 | nonce: U256::zero(), 135 | action: Action::Call(contract), 136 | }; 137 | api::send_transaction_with_nonce(self.app.connections.foreign.clone(), self.app.clone(), self.app.config.foreign.clone(), 138 | tx, self.foreign_chain_id, SendRawTransaction(self.app.connections.foreign.clone())) 139 | }).collect_vec(); 140 | 141 | info!("submitting {} signatures", len); 142 | WithdrawConfirmState::ConfirmWithdraws { 143 | future: futures_unordered(confirmations).collect(), 144 | block, 145 | } 146 | }, 147 | WithdrawConfirmState::ConfirmWithdraws { ref mut future, block } => { 148 | let _ = try_ready!(future.poll().map_err(|e| ErrorKind::ContextualizedError(Box::new(e), "sending signature submissions to foreign"))); 149 | info!("submitting signatures complete"); 150 | WithdrawConfirmState::Yield(Some(block)) 151 | }, 152 | WithdrawConfirmState::Yield(ref mut block) => match block.take() { 153 | None => { 154 | info!("waiting for new withdraws that should get signed"); 155 | WithdrawConfirmState::Wait 156 | }, 157 | Some(v) => return Ok(Some(BridgeChecked::WithdrawConfirm(v)).into()), 158 | } 159 | }; 160 | self.state = next_state; 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /bridge/src/bridge/withdraw_relay.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, RwLock}; 2 | use futures::{self, Future, Stream, stream::{Collect, FuturesUnordered, futures_unordered}, Poll}; 3 | use futures::future::{JoinAll, join_all, Join}; 4 | use tokio_timer::Timeout; 5 | use web3::Transport; 6 | use web3::types::{U256, Address, FilterBuilder, Log, Bytes}; 7 | use ethabi::{RawLog, self}; 8 | use app::App; 9 | use api::{self, LogStream, ApiCall}; 10 | use contracts::foreign; 11 | use util::web3_filter; 12 | use database::Database; 13 | use error::{self, Error, ErrorKind}; 14 | use message_to_mainnet::MessageToMainnet; 15 | use signature::Signature; 16 | use ethcore_transaction::{Transaction, Action}; 17 | use super::nonce::{NonceCheck, SendRawTransaction}; 18 | use super::BridgeChecked; 19 | use itertools::Itertools; 20 | 21 | /// returns a filter for `ForeignBridge.CollectedSignatures` events 22 | fn collected_signatures_filter>(foreign: &foreign::ForeignBridge, addresses: I) -> FilterBuilder { 23 | let filter = foreign.events().collected_signatures().create_filter(); 24 | web3_filter(filter, addresses) 25 | } 26 | 27 | /// payloads for calls to `ForeignBridge.signature` and `ForeignBridge.message` 28 | /// to retrieve the signatures (v, r, s) and messages 29 | /// which the withdraw relay process should later relay to `HomeBridge` 30 | /// by calling `HomeBridge.withdraw(v, r, s, message)` 31 | #[derive(Debug, PartialEq)] 32 | struct RelayAssignment { 33 | signature_payloads: Vec, 34 | message_payload: Bytes, 35 | } 36 | 37 | fn signatures_payload(foreign: &foreign::ForeignBridge, my_address: Address, log: Log) -> error::Result> { 38 | // convert web3::Log to ethabi::RawLog since ethabi events can 39 | // only be parsed from the latter 40 | let raw_log = RawLog { 41 | topics: log.topics.into_iter().map(|t| t.0.into()).collect(), 42 | data: log.data.0, 43 | }; 44 | let collected_signatures = foreign.events().collected_signatures().parse_log(raw_log)?; 45 | if collected_signatures.authority_responsible_for_relay != my_address.0.into() { 46 | info!("bridge not responsible for relaying transaction to home. tx hash: {}", log.transaction_hash.unwrap()); 47 | // this authority is not responsible for relaying this transaction. 48 | // someone else will relay this transaction to home. 49 | return Ok(None); 50 | } 51 | 52 | let required_signatures: U256 = (&foreign.functions().message().input(collected_signatures.number_of_collected_signatures)[4..]).into(); 53 | let signature_payloads = (0..required_signatures.low_u32()).into_iter() 54 | .map(|index| foreign.functions().signature().input(collected_signatures.message_hash, index)) 55 | .map(Into::into) 56 | .collect(); 57 | let message_payload = foreign.functions().message().input(collected_signatures.message_hash).into(); 58 | 59 | Ok(Some(RelayAssignment { 60 | signature_payloads, 61 | message_payload, 62 | })) 63 | } 64 | 65 | /// state of the withdraw relay state machine 66 | pub enum WithdrawRelayState { 67 | Wait, 68 | FetchMessagesSignatures { 69 | future: Join< 70 | JoinAll>>>, 71 | JoinAll>>>>> 72 | >, 73 | block: u64, 74 | }, 75 | RelayWithdraws { 76 | future: Collect>>>, 77 | block: u64, 78 | }, 79 | Yield(Option), 80 | } 81 | 82 | pub fn create_withdraw_relay(app: Arc>, init: &Database, home_balance: Arc>>, home_chain_id: u64, home_gas_price: Arc>) -> WithdrawRelay { 83 | let logs_init = api::LogStreamInit { 84 | after: init.checked_withdraw_relay, 85 | request_timeout: app.config.foreign.request_timeout, 86 | poll_interval: app.config.foreign.poll_interval, 87 | confirmations: app.config.foreign.required_confirmations, 88 | filter: collected_signatures_filter(&app.foreign_bridge, vec![init.foreign_contract_address]), 89 | }; 90 | 91 | WithdrawRelay { 92 | logs: api::log_stream(app.connections.foreign.clone(), app.timer.clone(), logs_init), 93 | home_contract: init.home_contract_address, 94 | foreign_contract: init.foreign_contract_address, 95 | state: WithdrawRelayState::Wait, 96 | app, 97 | home_balance, 98 | home_chain_id, 99 | home_gas_price, 100 | } 101 | } 102 | 103 | pub struct WithdrawRelay { 104 | app: Arc>, 105 | logs: LogStream, 106 | state: WithdrawRelayState, 107 | foreign_contract: Address, 108 | home_contract: Address, 109 | home_balance: Arc>>, 110 | home_chain_id: u64, 111 | home_gas_price: Arc>, 112 | } 113 | 114 | impl Stream for WithdrawRelay { 115 | type Item = BridgeChecked; 116 | type Error = Error; 117 | 118 | fn poll(&mut self) -> Poll, Self::Error> { 119 | let app = &self.app; 120 | let gas = self.app.config.txs.withdraw_relay.gas.into(); 121 | let gas_price = U256::from(*self.home_gas_price.read().unwrap()); 122 | let contract = self.home_contract.clone(); 123 | let home = &self.app.config.home; 124 | let t = &self.app.connections.home; 125 | let foreign = &self.app.connections.foreign; 126 | let chain_id = self.home_chain_id; 127 | let foreign_bridge = &self.app.foreign_bridge; 128 | let foreign_account = self.app.config.foreign.account; 129 | let timer = &self.app.timer; 130 | let foreign_contract = self.foreign_contract; 131 | let foreign_request_timeout = self.app.config.foreign.request_timeout; 132 | 133 | loop { 134 | let next_state = match self.state { 135 | WithdrawRelayState::Wait => { 136 | let item = try_stream!(self.logs.poll().map_err(|e| ErrorKind::ContextualizedError(Box::new(e), "polling foreign for collected signatures"))); 137 | info!("got {} new signed withdraws to relay", item.logs.len()); 138 | let assignments = item.logs 139 | .into_iter() 140 | .map(|log| signatures_payload( 141 | foreign_bridge, 142 | foreign_account, 143 | log)) 144 | .collect::>>()?; 145 | 146 | let (signatures, messages): (Vec<_>, Vec<_>) = assignments.into_iter() 147 | .filter_map(|a| a) 148 | .map(|assignment| (assignment.signature_payloads, assignment.message_payload)) 149 | .unzip(); 150 | 151 | let message_calls = messages.into_iter() 152 | .map(|payload| { 153 | timer.timeout( 154 | api::call(foreign, foreign_contract.clone(), payload), 155 | foreign_request_timeout) 156 | }) 157 | .collect::>(); 158 | 159 | let signature_calls = signatures.into_iter() 160 | .map(|payloads| { 161 | payloads.into_iter() 162 | .map(|payload| { 163 | timer.timeout( 164 | api::call(foreign, foreign_contract.clone(), payload), 165 | foreign_request_timeout) 166 | }) 167 | .collect::>() 168 | }) 169 | .map(|calls| join_all(calls)) 170 | .collect::>(); 171 | 172 | info!("fetching messages and signatures"); 173 | WithdrawRelayState::FetchMessagesSignatures { 174 | future: join_all(message_calls).join(join_all(signature_calls)), 175 | block: item.to, 176 | } 177 | }, 178 | WithdrawRelayState::FetchMessagesSignatures { ref mut future, block } => { 179 | let home_balance = self.home_balance.read().unwrap(); 180 | if home_balance.is_none() { 181 | warn!("home contract balance is unknown"); 182 | return Ok(futures::Async::NotReady); 183 | } 184 | 185 | let (messages_raw, signatures_raw) = try_ready!(future.poll().map_err(|e| ErrorKind::ContextualizedError(Box::new(e), "fetching messages and signatures from foreign"))); 186 | info!("fetching messages and signatures complete"); 187 | assert_eq!(messages_raw.len(), signatures_raw.len()); 188 | 189 | let balance_required = gas * gas_price * U256::from(messages_raw.len()); 190 | if balance_required > *home_balance.as_ref().unwrap() { 191 | return Err(ErrorKind::InsufficientFunds.into()) 192 | } 193 | 194 | let messages = messages_raw 195 | .iter() 196 | .map(|message| { 197 | app.foreign_bridge.functions().message().output(message.0.as_slice()).map(Bytes) 198 | }) 199 | .collect::>>() 200 | .map_err(error::Error::from)?; 201 | 202 | let len = messages.len(); 203 | 204 | let signatures = signatures_raw 205 | .iter() 206 | .map(|signatures| 207 | signatures.iter().map( 208 | |signature| { 209 | Signature::from_bytes( 210 | app.foreign_bridge 211 | .functions() 212 | .signature() 213 | .output(signature.0.as_slice())? 214 | .as_slice()) 215 | } 216 | ) 217 | .collect::, Error>>() 218 | .map_err(error::Error::from) 219 | ) 220 | .collect::>>()?; 221 | 222 | let relays = messages.into_iter() 223 | .zip(signatures.into_iter()) 224 | .map(|(message, signatures)| { 225 | let payload: Bytes = app.home_bridge.functions().withdraw().input( 226 | signatures.iter().map(|x| x.v), 227 | signatures.iter().map(|x| x.r), 228 | signatures.iter().map(|x| x.s), 229 | message.clone().0).into(); 230 | let gas_price = MessageToMainnet::from_bytes(message.0.as_slice()).mainnet_gas_price; 231 | let tx = Transaction { 232 | gas, 233 | gas_price, 234 | value: U256::zero(), 235 | data: payload.0, 236 | nonce: U256::zero(), 237 | action: Action::Call(contract), 238 | }; 239 | api::send_transaction_with_nonce(t.clone(), app.clone(), home.clone(), tx, chain_id, SendRawTransaction(t.clone())) 240 | }).collect_vec(); 241 | 242 | info!("relaying {} withdraws", len); 243 | WithdrawRelayState::RelayWithdraws { 244 | future: futures_unordered(relays).collect(), 245 | block, 246 | } 247 | }, 248 | WithdrawRelayState::RelayWithdraws { ref mut future, block } => { 249 | let _ = try_ready!(future.poll().map_err(|e| ErrorKind::ContextualizedError(Box::new(e), "sending withdrawal to home"))); 250 | info!("relaying withdraws complete"); 251 | WithdrawRelayState::Yield(Some(block)) 252 | }, 253 | WithdrawRelayState::Yield(ref mut block) => match block.take() { 254 | None => { 255 | info!("waiting for signed withdraws to relay"); 256 | WithdrawRelayState::Wait 257 | }, 258 | Some(v) => return Ok(Some(BridgeChecked::WithdrawRelay(v)).into()), 259 | } 260 | }; 261 | self.state = next_state; 262 | } 263 | } 264 | } 265 | 266 | #[cfg(test)] 267 | mod tests { 268 | use rustc_hex::FromHex; 269 | use web3::types::{Log, Bytes, Address}; 270 | use contracts::foreign; 271 | use super::signatures_payload; 272 | 273 | #[test] 274 | fn test_signatures_payload() { 275 | let foreign = foreign::ForeignBridge::default(); 276 | let my_address = "aff3454fce5edbc8cca8697c15331677e6ebcccc".into(); 277 | 278 | let data = "000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebcccc00000000000000000000000000000000000000000000000000000000000000f00000000000000000000000000000000000000000000000000000000000000002".from_hex().unwrap(); 279 | 280 | let log = Log { 281 | data: data.into(), 282 | topics: vec!["415557404d88a0c0b8e3b16967cafffc511213fd9c465c16832ee17ed57d7237".into()], 283 | transaction_hash: Some("884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a9424364".into()), 284 | address: Address::zero(), 285 | block_hash: None, 286 | transaction_index: None, 287 | log_index: None, 288 | transaction_log_index: None, 289 | log_type: None, 290 | block_number: None, 291 | removed: None, 292 | }; 293 | 294 | let assignment = signatures_payload(&foreign, my_address, log).unwrap().unwrap(); 295 | let expected_message: Bytes = "490a32c600000000000000000000000000000000000000000000000000000000000000f0".from_hex().unwrap().into(); 296 | let expected_signatures: Vec = vec![ 297 | "1812d99600000000000000000000000000000000000000000000000000000000000000f00000000000000000000000000000000000000000000000000000000000000000".from_hex().unwrap().into(), 298 | "1812d99600000000000000000000000000000000000000000000000000000000000000f00000000000000000000000000000000000000000000000000000000000000001".from_hex().unwrap().into(), 299 | ]; 300 | assert_eq!(expected_message, assignment.message_payload); 301 | assert_eq!(expected_signatures, assignment.signature_payloads); 302 | } 303 | 304 | #[test] 305 | fn test_signatures_payload_not_ours() { 306 | let foreign = foreign::ForeignBridge::default(); 307 | let my_address = "aff3454fce5edbc8cca8697c15331677e6ebcccd".into(); 308 | 309 | let data = "000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebcccc00000000000000000000000000000000000000000000000000000000000000f00000000000000000000000000000000000000000000000000000000000000002".from_hex().unwrap(); 310 | 311 | let log = Log { 312 | data: data.into(), 313 | topics: vec!["415557404d88a0c0b8e3b16967cafffc511213fd9c465c16832ee17ed57d7237".into()], 314 | transaction_hash: Some("884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a9424364".into()), 315 | address: Address::zero(), 316 | block_hash: None, 317 | transaction_index: None, 318 | log_index: None, 319 | transaction_log_index: None, 320 | log_type: None, 321 | block_number: None, 322 | removed: None, 323 | }; 324 | 325 | let assignment = signatures_payload(&foreign, my_address, log).unwrap(); 326 | assert_eq!(None, assignment); 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /bridge/src/contracts.rs: -------------------------------------------------------------------------------- 1 | use_contract!(home, "HomeBridge", "../compiled_contracts/HomeBridge.abi"); 2 | use_contract!(foreign, "ForeignBridge", "../compiled_contracts/ForeignBridge.abi"); 3 | use_contract!(erc20, "ERC20", "../compiled_contracts/ERC20.abi"); 4 | -------------------------------------------------------------------------------- /bridge/src/database.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::{io, str, fs, fmt}; 3 | use std::io::{Read, Write}; 4 | use web3::types::Address; 5 | use toml; 6 | use error::{Error, ResultExt, ErrorKind}; 7 | 8 | /// Application "database". 9 | #[derive(Debug, PartialEq, Deserialize, Serialize, Default, Clone)] 10 | pub struct Database { 11 | /// Address of home contract. 12 | pub home_contract_address: Address, 13 | /// Address of foreign contract. 14 | pub foreign_contract_address: Address, 15 | /// Number of block at which home contract has been deployed. 16 | pub home_deploy: Option, 17 | /// Number of block at which foreign contract has been deployed. 18 | pub foreign_deploy: Option, 19 | /// Number of last block which has been checked for deposit relays. 20 | pub checked_deposit_relay: u64, 21 | /// Number of last block which has been checked for withdraw relays. 22 | pub checked_withdraw_relay: u64, 23 | /// Number of last block which has been checked for withdraw confirms. 24 | pub checked_withdraw_confirm: u64, 25 | } 26 | 27 | impl str::FromStr for Database { 28 | type Err = Error; 29 | 30 | fn from_str(s: &str) -> Result { 31 | toml::from_str(s).chain_err(|| "Cannot parse database") 32 | } 33 | } 34 | 35 | impl fmt::Display for Database { 36 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 37 | f.write_str(&toml::to_string(self).expect("serialization can't fail; qed")) 38 | } 39 | } 40 | 41 | impl Database { 42 | pub fn load>(path: P) -> Result { 43 | let mut file = match fs::File::open(&path) { 44 | Ok(file) => file, 45 | Err(ref err) if err.kind() == io::ErrorKind::NotFound => return Err(ErrorKind::MissingFile(format!("{:?}", path.as_ref())).into()), 46 | Err(err) => return Err(err).chain_err(|| "Cannot open database"), 47 | }; 48 | 49 | let mut buffer = String::new(); 50 | file.read_to_string(&mut buffer)?; 51 | buffer.parse() 52 | } 53 | 54 | pub fn save(&self, mut write: W) -> Result<(), Error> { 55 | write.write_all(self.to_string().as_bytes())?; 56 | Ok(()) 57 | } 58 | } 59 | 60 | #[cfg(test)] 61 | mod tests { 62 | use super::Database; 63 | 64 | #[test] 65 | fn database_to_and_from_str() { 66 | let toml = 67 | r#"home_contract_address = "0x49edf201c1e139282643d5e7c6fb0c7219ad1db7" 68 | foreign_contract_address = "0x49edf201c1e139282643d5e7c6fb0c7219ad1db8" 69 | home_deploy = 100 70 | foreign_deploy = 101 71 | checked_deposit_relay = 120 72 | checked_withdraw_relay = 121 73 | checked_withdraw_confirm = 121 74 | "#; 75 | 76 | let expected = Database { 77 | home_contract_address: "49edf201c1e139282643d5e7c6fb0c7219ad1db7".into(), 78 | foreign_contract_address: "49edf201c1e139282643d5e7c6fb0c7219ad1db8".into(), 79 | home_deploy: Some(100), 80 | foreign_deploy: Some(101), 81 | checked_deposit_relay: 120, 82 | checked_withdraw_relay: 121, 83 | checked_withdraw_confirm: 121, 84 | }; 85 | 86 | let database = toml.parse().unwrap(); 87 | assert_eq!(expected, database); 88 | let s = database.to_string(); 89 | assert_eq!(s, toml); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /bridge/src/error.rs: -------------------------------------------------------------------------------- 1 | #![allow(unknown_lints)] 2 | 3 | use std::io; 4 | use tokio_timer::{TimerError, TimeoutError}; 5 | use {web3, toml, ethabi, rustc_hex}; 6 | use ethcore::ethstore; 7 | use ethcore::account_provider::{SignError, Error as AccountError}; 8 | use serde_json; 9 | use hyper; 10 | 11 | error_chain! { 12 | types { 13 | Error, ErrorKind, ResultExt, Result; 14 | } 15 | 16 | foreign_links { 17 | Io(io::Error); 18 | Toml(toml::de::Error); 19 | Ethabi(ethabi::Error); 20 | Timer(TimerError); 21 | Hex(rustc_hex::FromHexError); 22 | Json(serde_json::Error); 23 | Hyper(hyper::Error); 24 | } 25 | 26 | errors { 27 | ShutdownRequested 28 | InsufficientFunds 29 | NoRequiredSignaturesChanged { 30 | description("No RequiredSignaturesChanged has been observed") 31 | } 32 | // api timeout 33 | Timeout(request: &'static str) { 34 | description("Request timeout"), 35 | display("Request {} timed out", request), 36 | } 37 | // workaround for error_chain not allowing to check internal error kind 38 | // https://github.com/rust-lang-nursery/error-chain/issues/206 39 | MissingFile(filename: String) { 40 | description("File not found"), 41 | display("File {} not found", filename), 42 | } 43 | // workaround for lack of web3:Error Display and Error implementations 44 | Web3(err: web3::Error) { 45 | description("web3 error"), 46 | display("{:?}", err), 47 | } 48 | KeyStore(err: ethstore::Error) { 49 | description("keystore error"), 50 | display("keystore error {:?}", err), 51 | } 52 | SignError(err: SignError) { 53 | description("signing error") 54 | display("signing error {:?}", err), 55 | } 56 | AccountError(err: AccountError) { 57 | description("account error") 58 | display("account error {:?}", err), 59 | } 60 | ContextualizedError(err: Box, context: &'static str) { 61 | description("contextualized error") 62 | display("{:?} in {}", err, context) 63 | } 64 | OtherError(error: String) { 65 | description("other error") 66 | display("{}", error) 67 | } 68 | ConfigError(err: String) { 69 | description("config error") 70 | display("{}", err) 71 | } 72 | } 73 | } 74 | 75 | impl From> for Error { 76 | fn from(err: TimeoutError) -> Self { 77 | match err { 78 | TimeoutError::Timer(_call, _) | TimeoutError::TimedOut(_call) => { 79 | ErrorKind::Timeout("communication timeout").into() 80 | } 81 | } 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /bridge/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit="128"] 2 | #[macro_use] 3 | extern crate futures; 4 | extern crate serde; 5 | #[macro_use] 6 | extern crate serde_derive; 7 | extern crate serde_json; 8 | extern crate toml; 9 | pub extern crate web3; 10 | extern crate tokio_core; 11 | extern crate tokio_timer; 12 | #[macro_use] 13 | extern crate error_chain; 14 | extern crate ethabi; 15 | #[macro_use] 16 | extern crate ethabi_derive; 17 | #[macro_use] 18 | extern crate ethabi_contract; 19 | extern crate rustc_hex; 20 | #[macro_use] 21 | extern crate log; 22 | extern crate ethereum_types; 23 | #[macro_use] 24 | extern crate pretty_assertions; 25 | 26 | extern crate ethcore; 27 | extern crate ethcore_transaction; 28 | extern crate rlp; 29 | extern crate keccak_hash; 30 | extern crate jsonrpc_core as rpc; 31 | 32 | extern crate itertools; 33 | extern crate hyper; 34 | extern crate hyper_tls; 35 | 36 | #[cfg(test)] 37 | #[macro_use] 38 | extern crate quickcheck; 39 | 40 | #[macro_use] 41 | mod macros; 42 | 43 | pub mod api; 44 | pub mod app; 45 | pub mod config; 46 | pub mod bridge; 47 | pub mod contracts; 48 | pub mod database; 49 | pub mod error; 50 | pub mod util; 51 | pub mod message_to_mainnet; 52 | pub mod signature; 53 | pub mod transaction; 54 | -------------------------------------------------------------------------------- /bridge/src/macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! try_bridge { 2 | ($e: expr) => (match $e { 3 | Err(err) => return Err(From::from(err)), 4 | Ok($crate::futures::Async::NotReady) => None, 5 | Ok($crate::futures::Async::Ready(None)) => return Ok($crate::futures::Async::Ready(None)), 6 | Ok($crate::futures::Async::Ready(Some(value))) => Some(value), 7 | }) 8 | } 9 | 10 | macro_rules! try_stream { 11 | ($e: expr) => (match $e { 12 | Err(err) => return Err(From::from(err)), 13 | Ok($crate::futures::Async::NotReady) => return Ok($crate::futures::Async::NotReady), 14 | Ok($crate::futures::Async::Ready(None)) => return Ok($crate::futures::Async::Ready(None)), 15 | Ok($crate::futures::Async::Ready(Some(value))) => value, 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /bridge/src/message_to_mainnet.rs: -------------------------------------------------------------------------------- 1 | use ethereum_types::{Address, U256, H256}; 2 | use contracts::foreign::events::Withdraw; 3 | use web3::types::Log; 4 | use ethabi; 5 | use error::Error; 6 | 7 | /// the message that is relayed from side to main. 8 | /// contains all the information required for the relay. 9 | /// validators sign off on this message. 10 | #[derive(PartialEq, Debug)] 11 | pub struct MessageToMainnet { 12 | pub recipient: Address, 13 | pub value: U256, 14 | pub sidenet_transaction_hash: H256, 15 | pub mainnet_gas_price: U256, 16 | } 17 | 18 | /// length of a `MessageToMainnet.to_bytes()` in bytes 19 | pub const MESSAGE_LENGTH: usize = 116; 20 | 21 | impl MessageToMainnet { 22 | /// parses message from a byte slice 23 | pub fn from_bytes(bytes: &[u8]) -> Self { 24 | assert_eq!(bytes.len(), MESSAGE_LENGTH); 25 | 26 | Self { 27 | recipient: bytes[0..20].into(), 28 | value: (&bytes[20..52]).into(), 29 | sidenet_transaction_hash: bytes[52..84].into(), 30 | mainnet_gas_price: (&bytes[84..MESSAGE_LENGTH]).into(), 31 | } 32 | } 33 | 34 | /// construct a message from a `Withdraw` event that was logged on `foreign` 35 | pub fn from_log(web3_log: Log) -> Result { 36 | let ethabi_raw_log = ethabi::RawLog { 37 | topics: web3_log.topics, 38 | data: web3_log.data.0, 39 | }; 40 | let withdraw_log = Withdraw::default().parse_log(ethabi_raw_log)?; 41 | let hash = web3_log.transaction_hash.ok_or_else(|| "`log` must be mined and contain `transaction_hash`")?; 42 | Ok(Self { 43 | recipient: withdraw_log.recipient, 44 | value: withdraw_log.value, 45 | sidenet_transaction_hash: hash, 46 | mainnet_gas_price: withdraw_log.home_gas_price, 47 | }) 48 | } 49 | 50 | /// serializes message to a byte vector. 51 | /// mainly used to construct the message byte vector that is then signed 52 | /// and passed to `ForeignBridge.submitSignature` 53 | pub fn to_bytes(&self) -> Vec { 54 | let mut result = vec![0u8; MESSAGE_LENGTH]; 55 | result[0..20].copy_from_slice(&self.recipient.0[..]); 56 | result[20..52].copy_from_slice(&H256::from(self.value)); 57 | result[52..84].copy_from_slice(&self.sidenet_transaction_hash.0[..]); 58 | result[84..MESSAGE_LENGTH].copy_from_slice(&H256::from(self.mainnet_gas_price)); 59 | return result; 60 | } 61 | 62 | /// serializes message to an ethabi payload 63 | pub fn to_payload(&self) -> Vec { 64 | ethabi::encode(&[ethabi::Token::Bytes(self.to_bytes())]) 65 | } 66 | } 67 | 68 | #[cfg(test)] 69 | mod test { 70 | use quickcheck::TestResult; 71 | use super::*; 72 | 73 | quickcheck! { 74 | fn quickcheck_message_to_mainnet_roundtrips_to_bytes( 75 | recipient_raw: Vec, 76 | value_raw: u64, 77 | sidenet_transaction_hash_raw: Vec, 78 | mainnet_gas_price_raw: u64 79 | ) -> TestResult { 80 | if recipient_raw.len() != 20 || sidenet_transaction_hash_raw.len() != 32 { 81 | return TestResult::discard(); 82 | } 83 | 84 | let recipient: Address = recipient_raw.as_slice().into(); 85 | let value: U256 = value_raw.into(); 86 | let sidenet_transaction_hash: H256 = sidenet_transaction_hash_raw.as_slice().into(); 87 | let mainnet_gas_price: U256 = mainnet_gas_price_raw.into(); 88 | 89 | let message = MessageToMainnet { 90 | recipient, 91 | value, 92 | sidenet_transaction_hash, 93 | mainnet_gas_price 94 | }; 95 | 96 | let bytes = message.to_bytes(); 97 | assert_eq!(message, MessageToMainnet::from_bytes(bytes.as_slice())); 98 | 99 | let payload = message.to_payload(); 100 | let mut tokens = ethabi::decode(&[ethabi::ParamType::Bytes], payload.as_slice()) 101 | .unwrap(); 102 | let decoded = tokens.pop().unwrap().to_bytes().unwrap(); 103 | assert_eq!(message, MessageToMainnet::from_bytes(decoded.as_slice())); 104 | 105 | TestResult::passed() 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /bridge/src/signature.rs: -------------------------------------------------------------------------------- 1 | /// ECDSA signatures: 2 | /// conversion from/to byte vectors. 3 | /// from/to v, r, s components. 4 | 5 | use ethereum_types::H256; 6 | use ethabi; 7 | 8 | use error::Error; 9 | 10 | pub const SIGNATURE_LENGTH: usize = 65; 11 | 12 | /// an ECDSA signature consisting of `v`, `r` and `s` 13 | #[derive(PartialEq, Debug)] 14 | pub struct Signature { 15 | pub v: u8, 16 | pub r: H256, 17 | pub s: H256, 18 | } 19 | 20 | impl Signature { 21 | pub fn from_bytes(bytes: &[u8]) -> Result { 22 | if bytes.len() != SIGNATURE_LENGTH { 23 | bail!("`bytes`.len() must be {}", SIGNATURE_LENGTH); 24 | } 25 | 26 | Ok(Self { 27 | v: bytes[64], 28 | r: bytes[0..32].into(), 29 | s: bytes[32..64].into(), 30 | }) 31 | } 32 | 33 | pub fn to_bytes(&self) -> Vec { 34 | let mut result = vec![0u8; SIGNATURE_LENGTH]; 35 | result[0..32].copy_from_slice(&self.r.0[..]); 36 | result[32..64].copy_from_slice(&self.s.0[..]); 37 | result[64] = self.v; 38 | return result; 39 | } 40 | 41 | pub fn to_payload(&self) -> Vec { 42 | ethabi::encode(&[ethabi::Token::Bytes(self.to_bytes())]) 43 | } 44 | } 45 | 46 | #[cfg(test)] 47 | mod test { 48 | use quickcheck::TestResult; 49 | use super::*; 50 | 51 | quickcheck! { 52 | fn quickcheck_signature_roundtrips(v: u8, r_raw: Vec, s_raw: Vec) -> TestResult { 53 | if r_raw.len() != 32 || s_raw.len() != 32 { 54 | return TestResult::discard(); 55 | } 56 | 57 | let r: H256 = r_raw.as_slice().into(); 58 | let s: H256 = s_raw.as_slice().into(); 59 | let signature = Signature { v, r, s }; 60 | assert_eq!(v, signature.v); 61 | assert_eq!(r, signature.r); 62 | assert_eq!(s, signature.s); 63 | 64 | let bytes = signature.to_bytes(); 65 | 66 | assert_eq!(signature, Signature::from_bytes(bytes.as_slice()).unwrap()); 67 | 68 | let payload = signature.to_payload(); 69 | let mut tokens = ethabi::decode(&[ethabi::ParamType::Bytes], payload.as_slice()) 70 | .unwrap(); 71 | let decoded = tokens.pop().unwrap().to_bytes().unwrap(); 72 | assert_eq!(signature, Signature::from_bytes(decoded.as_slice()).unwrap()); 73 | 74 | TestResult::passed() 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /bridge/src/transaction.rs: -------------------------------------------------------------------------------- 1 | use error::{Error, ErrorKind}; 2 | use ethcore_transaction::{Transaction, SignedTransaction}; 3 | use web3::types::Bytes; 4 | use config::Node; 5 | use app::App; 6 | use web3::Transport; 7 | 8 | pub fn prepare_raw_transaction(tx: Transaction, app: &App, node: &Node, chain_id: u64) -> Result { 9 | let hash = tx.hash(Some(chain_id)); 10 | 11 | let sig = app.keystore.sign(node.account, None, hash).map_err(|e| ErrorKind::SignError(e))?; 12 | let tx = SignedTransaction::new(tx.with_signature(sig, Some(chain_id))).unwrap(); 13 | 14 | use rlp::{RlpStream, Encodable}; 15 | let mut stream = RlpStream::new(); 16 | tx.rlp_append(&mut stream); 17 | 18 | Ok(Bytes(stream.out())) 19 | } 20 | -------------------------------------------------------------------------------- /bridge/src/util.rs: -------------------------------------------------------------------------------- 1 | use web3::types::{H256, Address, FilterBuilder}; 2 | use ethabi; 3 | 4 | fn web3_topic(topic: ethabi::Topic) -> Option> { 5 | let t: Vec = topic.into(); 6 | // parity does not conform to an ethereum spec 7 | if t.is_empty() { 8 | None 9 | } else { 10 | Some(t) 11 | } 12 | } 13 | 14 | pub fn web3_filter>(filter: ethabi::TopicFilter, addresses: I) -> FilterBuilder { 15 | let t0 = web3_topic(filter.topic0); 16 | let t1 = web3_topic(filter.topic1); 17 | let t2 = web3_topic(filter.topic2); 18 | let t3 = web3_topic(filter.topic3); 19 | FilterBuilder::default() 20 | .address(addresses.into_iter().collect()) 21 | .topics(t0, t1, t2, t3) 22 | } 23 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bridge-cli" 3 | version = "0.3.0" 4 | 5 | [[bin]] 6 | name = "bridge" 7 | path = "src/main.rs" 8 | 9 | [dependencies] 10 | bridge = { path = "../bridge" } 11 | serde = "1.0" 12 | serde_derive = "1.0" 13 | tokio-core = "0.1.8" 14 | docopt = "0.8.1" 15 | log = "0.3" 16 | env_logger = "0.4" 17 | futures = "0.1.14" 18 | jsonrpc-core = "8.0" 19 | ctrlc = { version = "3.1", features = ["termination"] } 20 | version = "3" 21 | 22 | [features] 23 | default = [] 24 | deploy = ["bridge/deploy"] 25 | -------------------------------------------------------------------------------- /cli/build.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | fn main() { 4 | let cmd = Command::new("git").args(&["describe", "--long", "--tags", "--always", "--dirty=-modified"]).output().unwrap(); 5 | if cmd.status.success() { 6 | // if we're successful, use this as a version 7 | let ver = std::str::from_utf8(&cmd.stdout[1..]).unwrap().trim(); // drop "v" in the front 8 | println!("cargo:rustc-env={}={}", "CARGO_PKG_VERSION", ver); 9 | } 10 | // otherwise, whatever is specified in Cargo manifest 11 | println!("cargo:rerun-if-changed=nonexistentfile"); // always rerun build.rs 12 | } 13 | -------------------------------------------------------------------------------- /cli/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate serde; 2 | #[macro_use] 3 | extern crate serde_derive; 4 | extern crate docopt; 5 | extern crate futures; 6 | extern crate tokio_core; 7 | #[macro_use] 8 | extern crate log; 9 | extern crate env_logger; 10 | extern crate bridge; 11 | extern crate ctrlc; 12 | extern crate jsonrpc_core as rpc; 13 | #[macro_use] 14 | extern crate version; 15 | 16 | use std::{env, fs, io}; 17 | use std::sync::Arc; 18 | use std::path::PathBuf; 19 | use docopt::Docopt; 20 | use futures::{Stream, future}; 21 | use tokio_core::reactor::Core; 22 | 23 | use bridge::app::App; 24 | use bridge::bridge::{create_bridge, create_deploy, create_chain_id_retrieval, Deployed}; 25 | use bridge::config::Config; 26 | use bridge::error::{Error, ErrorKind}; 27 | use bridge::web3; 28 | 29 | const ERR_UNKNOWN: i32 = 1; 30 | const ERR_IO_ERROR: i32 = 2; 31 | const ERR_SHUTDOWN_REQUESTED: i32 = 3; 32 | const ERR_INSUFFICIENT_FUNDS: i32 = 4; 33 | const ERR_GAS_TOO_LOW: i32 = 5; 34 | const ERR_GAS_PRICE_TOO_LOW: i32 = 6; 35 | const ERR_NONCE_REUSE: i32 = 7; 36 | const ERR_CANNOT_CONNECT: i32 = 10; 37 | const ERR_CONNECTION_LOST: i32 = 11; 38 | const ERR_BRIDGE_CRASH: i32 = 12; 39 | const ERR_RPC_ERROR: i32 = 20; 40 | 41 | pub struct UserFacingError(i32, Error); 42 | 43 | impl From for UserFacingError { 44 | fn from(err: Error) -> Self { 45 | UserFacingError(ERR_UNKNOWN, err) 46 | } 47 | } 48 | 49 | impl From for UserFacingError { 50 | fn from(err: String) -> Self { 51 | UserFacingError(ERR_UNKNOWN, err.into()) 52 | } 53 | } 54 | 55 | 56 | impl From for UserFacingError { 57 | fn from(err: io::Error) -> Self { 58 | UserFacingError(ERR_IO_ERROR, err.into()) 59 | } 60 | } 61 | 62 | 63 | impl From<(i32, Error)> for UserFacingError { 64 | fn from((code, err): (i32, Error)) -> Self { 65 | UserFacingError(code, err) 66 | } 67 | } 68 | 69 | 70 | const USAGE: &'static str = r#" 71 | POA-Ethereum bridge. 72 | Copyright 2017 Parity Technologies (UK) Limited 73 | Copyright 2018 POA Networks Ltd. 74 | 75 | Usage: 76 | bridge [options] --config --database 77 | bridge -h | --help 78 | bridge -v | --version 79 | 80 | Options: 81 | -h, --help Display help message and exit. 82 | -v, --version Print version and exit. 83 | --allow-insecure-rpc-endpoints Allow non-HTTPS endpoints 84 | "#; 85 | 86 | #[derive(Debug, Deserialize)] 87 | pub struct Args { 88 | arg_config: PathBuf, 89 | arg_database: PathBuf, 90 | flag_version: bool, 91 | flag_allow_insecure_rpc_endpoints: bool, 92 | } 93 | 94 | use std::sync::atomic::{AtomicBool, Ordering}; 95 | 96 | fn main() { 97 | let _ = env_logger::init(); 98 | 99 | let running = Arc::new(AtomicBool::new(true)); 100 | 101 | let r = running.clone(); 102 | ctrlc::set_handler(move || { 103 | r.store(false, Ordering::SeqCst); 104 | }).expect("Error setting Ctrl-C handler"); 105 | 106 | let result = execute(env::args(), running); 107 | 108 | match result { 109 | Ok(s) => println!("{}", s), 110 | Err(UserFacingError(code, err)) => { 111 | print_err(err); 112 | ::std::process::exit(code); 113 | }, 114 | } 115 | } 116 | 117 | 118 | fn print_err(err: Error) { 119 | let message = err.iter().map(|e| e.to_string()).collect::>().join("\n\nCaused by:\n "); 120 | println!("{}", message); 121 | } 122 | 123 | fn execute(command: I, running: Arc) -> Result where I: IntoIterator, S: AsRef { 124 | info!(target: "bridge", "Parsing cli arguments"); 125 | let args: Args = Docopt::new(USAGE) 126 | .and_then(|d| d.argv(command).deserialize()).map_err(|e| e.to_string())?; 127 | 128 | if args.flag_version { 129 | return Ok(version!().into()) 130 | } 131 | 132 | info!(target: "bridge", "Loading config"); 133 | let config = Config::load(args.arg_config, args.flag_allow_insecure_rpc_endpoints)?; 134 | 135 | info!(target: "bridge", "Starting event loop"); 136 | let mut event_loop = Core::new().unwrap(); 137 | let handle = event_loop.handle(); 138 | 139 | info!(target: "bridge", "Home rpc host {}", config.clone().home.rpc_host); 140 | info!(target: "bridge", "Foreign rpc host {}", config.clone().foreign.rpc_host); 141 | 142 | info!(target: "bridge", "Establishing connection:"); 143 | 144 | info!(target:"bridge", " using RPC connection"); 145 | let app = match App::new_http(config.clone(), &args.arg_database, &handle, running.clone()) { 146 | Ok(app) => app, 147 | Err(e) => { 148 | warn!("Can't establish an RPC connection: {:?}", e); 149 | return Err((ERR_CANNOT_CONNECT, e).into()); 150 | }, 151 | }; 152 | 153 | let app = Arc::new(app); 154 | 155 | info!(target: "bridge", "Acquiring home & foreign chain ids"); 156 | let home_chain_id = event_loop.run(create_chain_id_retrieval(app.clone(), app.connections.home.clone(), app.config.home.clone())).expect("can't retrieve home chain_id"); 157 | let foreign_chain_id = event_loop.run(create_chain_id_retrieval(app.clone(), app.connections.foreign.clone(), app.config.foreign.clone())).expect("can't retrieve foreign chain_id"); 158 | 159 | info!(target: "bridge", "Home chain ID: {} Foreign chain ID: {}", home_chain_id, foreign_chain_id); 160 | 161 | { 162 | use bridge::api; 163 | let mut home_nonce = app.config.home.info.nonce.write().unwrap(); 164 | let mut foreign_nonce = app.config.foreign.info.nonce.write().unwrap(); 165 | 166 | *home_nonce = event_loop.run(api::eth_get_transaction_count(app.connections.home.clone(), app.config.home.account, None)).expect("can't initialize home nonce"); 167 | *foreign_nonce = event_loop.run(api::eth_get_transaction_count(app.connections.foreign.clone(), app.config.foreign.account, None)).expect("can't initialize foreign nonce"); 168 | } 169 | 170 | #[cfg(feature = "deploy")] 171 | info!(target: "bridge", "Deploying contracts (if needed)"); 172 | #[cfg(not(feature = "deploy"))] 173 | info!(target: "bridge", "Reading the database"); 174 | 175 | let deployed = event_loop.run(create_deploy(app.clone(), home_chain_id, foreign_chain_id))?; 176 | 177 | let database = match deployed { 178 | Deployed::New(database) => { 179 | info!(target: "bridge", "Deployed new bridge contracts"); 180 | info!(target: "bridge", "\n\n{}\n", database); 181 | database.save(fs::File::create(&app.database_path)?)?; 182 | database 183 | }, 184 | Deployed::Existing(database) => { 185 | info!(target: "bridge", "Loaded database"); 186 | database 187 | }, 188 | }; 189 | 190 | info!(target: "bridge", "Starting listening to events"); 191 | let bridge = create_bridge(app.clone(), &database, &handle, home_chain_id, foreign_chain_id).and_then(|_| future::ok(true)).collect(); 192 | let mut result = event_loop.run(bridge); 193 | loop { 194 | match result { 195 | Err(Error(ErrorKind::ContextualizedError(e, context), _)) => { 196 | error!("ERROR CONTEXT: {}", context); 197 | result = Err(*e); 198 | continue; 199 | } 200 | Err(Error(ErrorKind::Web3(web3::error::Error(web3::error::ErrorKind::Io(e), _)), _)) => { 201 | if e.kind() == ::std::io::ErrorKind::BrokenPipe { 202 | error!("Connection to a node has been severed"); 203 | return Err((ERR_CONNECTION_LOST, e.into()).into()); 204 | } else { 205 | error!("I/O error: {:?}", e); 206 | return Err((ERR_IO_ERROR, e.into()).into()); 207 | } 208 | }, 209 | Err(e @ Error(ErrorKind::ShutdownRequested, _)) => { 210 | info!("Shutdown requested, terminating"); 211 | return Err((ERR_SHUTDOWN_REQUESTED, e.into()).into()); 212 | }, 213 | Err(e @ Error(ErrorKind::InsufficientFunds, _)) => { 214 | error!("Insufficient funds, terminating"); 215 | return Err((ERR_INSUFFICIENT_FUNDS, e.into()).into()); 216 | }, 217 | Err(Error(ErrorKind::Web3(web3::error::Error(web3::error::ErrorKind::Rpc(e), _)), _)) => { 218 | if e.code == rpc::ErrorCode::ServerError(-32010) && e.message.starts_with("Insufficient funds") { 219 | error!("Insufficient funds, terminating"); 220 | return Err((ERR_INSUFFICIENT_FUNDS, ErrorKind::Web3(web3::error::ErrorKind::Rpc(e).into()).into()).into()); 221 | } else if e.code == rpc::ErrorCode::ServerError(-32010) && e.message.starts_with("Transaction gas is too low") { 222 | error!("Transaction gas is too low"); 223 | return Err((ERR_GAS_TOO_LOW, ErrorKind::Web3(web3::error::ErrorKind::Rpc(e).into()).into()).into()); 224 | } else if e.code == rpc::ErrorCode::ServerError(-32010) && e.message.starts_with("Transaction gas price is too low") { 225 | error!("Transaction gas price is too low"); 226 | return Err((ERR_GAS_PRICE_TOO_LOW, ErrorKind::Web3(web3::error::ErrorKind::Rpc(e).into()).into()).into()); 227 | } else if e.code == rpc::ErrorCode::ServerError(-32010) && e.message.starts_with("Transaction gas price is too low. There is another") { 228 | error!("Nonce reuse"); 229 | return Err((ERR_NONCE_REUSE, ErrorKind::Web3(web3::error::ErrorKind::Rpc(e).into()).into()).into()); 230 | } else if e.code == rpc::ErrorCode::ServerError(-32010) && e.message.starts_with("Transaction nonce is too low") { 231 | error!("Nonce reuse"); 232 | return Err((ERR_NONCE_REUSE, ErrorKind::Web3(web3::error::ErrorKind::Rpc(e).into()).into()).into()); 233 | } else { 234 | error!("RPC error {:?}", e); 235 | return Err((ERR_RPC_ERROR, ErrorKind::Web3(web3::error::ErrorKind::Rpc(e).into()).into()).into()); 236 | } 237 | }, 238 | Err(e) => { 239 | error!("Bridge crashed with {}", e); 240 | return Err((ERR_BRIDGE_CRASH, e).into()); 241 | }, 242 | Ok(_) => break, 243 | } 244 | } 245 | 246 | Ok("Done".into()) 247 | } 248 | 249 | 250 | #[cfg(test)] 251 | mod tests { 252 | } 253 | -------------------------------------------------------------------------------- /contracts/BridgeableToken.sol: -------------------------------------------------------------------------------- 1 | /** 2 | * This smart contract code is Copyright 2017 TokenMarket Ltd. For more information see https://tokenmarket.net 3 | * 4 | * Licensed under the Apache License, version 2.0: https://github.com/TokenMarketNet/ico/blob/master/LICENSE.txt 5 | */ 6 | 7 | pragma solidity ^0.4.15; 8 | 9 | /** 10 | * Math operations with safety checks 11 | */ 12 | contract SafeMath { 13 | function safeMul(uint a, uint b) internal returns (uint) { 14 | uint c = a * b; 15 | assert(a == 0 || c / a == b); 16 | return c; 17 | } 18 | 19 | function safeDiv(uint a, uint b) internal returns (uint) { 20 | assert(b > 0); 21 | uint c = a / b; 22 | assert(a == b * c + a % b); 23 | return c; 24 | } 25 | 26 | function safeSub(uint a, uint b) internal returns (uint) { 27 | assert(b <= a); 28 | return a - b; 29 | } 30 | 31 | function safeAdd(uint a, uint b) internal returns (uint) { 32 | uint c = a + b; 33 | assert(c>=a && c>=b); 34 | return c; 35 | } 36 | 37 | function max64(uint64 a, uint64 b) internal constant returns (uint64) { 38 | return a >= b ? a : b; 39 | } 40 | 41 | function min64(uint64 a, uint64 b) internal constant returns (uint64) { 42 | return a < b ? a : b; 43 | } 44 | 45 | function max256(uint256 a, uint256 b) internal constant returns (uint256) { 46 | return a >= b ? a : b; 47 | } 48 | 49 | function min256(uint256 a, uint256 b) internal constant returns (uint256) { 50 | return a < b ? a : b; 51 | } 52 | 53 | } 54 | 55 | library SafeMathLib { 56 | 57 | function times(uint a, uint b) returns (uint) { 58 | uint c = a * b; 59 | assert(a == 0 || c / a == b); 60 | return c; 61 | } 62 | 63 | function minus(uint a, uint b) returns (uint) { 64 | assert(b <= a); 65 | return a - b; 66 | } 67 | 68 | function plus(uint a, uint b) returns (uint) { 69 | uint c = a + b; 70 | assert(c>=a); 71 | return c; 72 | } 73 | 74 | } 75 | 76 | contract ERC20Basic { 77 | uint256 public totalSupply; 78 | function balanceOf(address who) public constant returns (uint256); 79 | function transfer(address to, uint256 value) public returns (bool); 80 | event Transfer(address indexed from, address indexed to, uint256 value); 81 | } 82 | 83 | /** 84 | * @title ERC20 interface 85 | * @dev see https://github.com/ethereum/EIPs/issues/20 86 | */ 87 | contract ERC20 is ERC20Basic { 88 | function allowance(address owner, address spender) public constant returns (uint256); 89 | function transferFrom(address from, address to, uint256 value) public returns (bool); 90 | function approve(address spender, uint256 value) public returns (bool); 91 | event Approval(address indexed owner, address indexed spender, uint256 value); 92 | } 93 | 94 | /* 95 | * @title Ownable 96 | * @dev The Ownable contract has an owner address, and provides basic authorization control 97 | * functions, this simplifies the implementation of "user permissions". 98 | */ 99 | contract Ownable { 100 | address public owner; 101 | 102 | 103 | event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); 104 | 105 | 106 | /** 107 | * @dev The Ownable constructor sets the original `owner` of the contract to the sender 108 | * account. 109 | */ 110 | function Ownable() { 111 | owner = msg.sender; 112 | } 113 | 114 | 115 | /** 116 | * @dev Throws if called by any account other than the owner. 117 | */ 118 | modifier onlyOwner() { 119 | require(msg.sender == owner); 120 | _; 121 | } 122 | 123 | 124 | /** 125 | * @dev Allows the current owner to transfer control of the contract to a newOwner. 126 | * @param newOwner The address to transfer ownership to. 127 | */ 128 | function transferOwnership(address newOwner) onlyOwner public { 129 | require(newOwner != address(0)); 130 | OwnershipTransferred(owner, newOwner); 131 | owner = newOwner; 132 | } 133 | 134 | } 135 | 136 | /** 137 | * Standard ERC20 token with Short Hand Attack and approve() race condition mitigation. 138 | * 139 | * Based on code by FirstBlood: 140 | * https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol 141 | */ 142 | contract StandardToken is ERC20, SafeMath { 143 | 144 | /* Token supply got increased and a new owner received these tokens */ 145 | event Minted(address receiver, uint amount); 146 | 147 | /* Actual balances of token holders */ 148 | mapping(address => uint) balances; 149 | 150 | /* approve() allowances */ 151 | mapping (address => mapping (address => uint)) allowed; 152 | 153 | /* Interface declaration */ 154 | function isToken() public constant returns (bool weAre) { 155 | return true; 156 | } 157 | 158 | function transfer(address _to, uint _value) returns (bool success) { 159 | balances[msg.sender] = safeSub(balances[msg.sender], _value); 160 | balances[_to] = safeAdd(balances[_to], _value); 161 | Transfer(msg.sender, _to, _value); 162 | return true; 163 | } 164 | 165 | function transferFrom(address _from, address _to, uint _value) returns (bool success) { 166 | uint _allowance = allowed[_from][msg.sender]; 167 | 168 | balances[_to] = safeAdd(balances[_to], _value); 169 | balances[_from] = safeSub(balances[_from], _value); 170 | allowed[_from][msg.sender] = safeSub(_allowance, _value); 171 | Transfer(_from, _to, _value); 172 | return true; 173 | } 174 | 175 | function balanceOf(address _owner) constant returns (uint balance) { 176 | return balances[_owner]; 177 | } 178 | 179 | function approve(address _spender, uint _value) returns (bool success) { 180 | 181 | // To change the approve amount you first have to reduce the addresses` 182 | // allowance to zero by calling `approve(_spender, 0)` if it is not 183 | // already 0 to mitigate the race condition described here: 184 | // https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 185 | if ((_value != 0) && (allowed[msg.sender][_spender] != 0)) throw; 186 | 187 | allowed[msg.sender][_spender] = _value; 188 | Approval(msg.sender, _spender, _value); 189 | return true; 190 | } 191 | 192 | function allowance(address _owner, address _spender) constant returns (uint remaining) { 193 | return allowed[_owner][_spender]; 194 | } 195 | 196 | } 197 | 198 | /** 199 | * A token that can increase its supply by another contract. 200 | * 201 | * This allows uncapped crowdsale by dynamically increasing the supply when money pours in. 202 | * Only mint agents, contracts whitelisted by owner, can mint new tokens. 203 | * 204 | */ 205 | contract MintableToken is StandardToken, Ownable { 206 | 207 | bool public mintingFinished = false; 208 | 209 | /** List of agents that are allowed to create new tokens */ 210 | mapping (address => bool) public mintAgents; 211 | 212 | event MintingAgentChanged(address addr, bool state ); 213 | 214 | /** 215 | * Create new tokens and allocate them to an address.. 216 | * 217 | * Only callably by a crowdsale contract (mint agent). 218 | */ 219 | function mint(address receiver, uint amount) onlyMintAgent canMint public { 220 | 221 | uint arranged_amount = amount * 1 ether; 222 | 223 | totalSupply = safeAdd(totalSupply, arranged_amount); 224 | balances[receiver] = safeAdd(balances[receiver], arranged_amount); 225 | 226 | // This will make the mint transaction apper in EtherScan.io 227 | // We can remove this after there is a standardized minting event 228 | Transfer(0, receiver, arranged_amount); 229 | } 230 | 231 | /** 232 | * Owner can allow a crowdsale contract to mint new tokens. 233 | */ 234 | function setMintAgent(address addr, bool state) onlyOwner canMint public { 235 | mintAgents[addr] = state; 236 | MintingAgentChanged(addr, state); 237 | } 238 | 239 | modifier onlyMintAgent() { 240 | // Only crowdsale contracts are allowed to mint new tokens 241 | if(!mintAgents[msg.sender]) { 242 | throw; 243 | } 244 | _; 245 | } 246 | 247 | /** Make sure we are not done yet. */ 248 | modifier canMint() { 249 | if(mintingFinished) throw; 250 | _; 251 | } 252 | } 253 | 254 | contract ApproveAndCallFallBack { 255 | function receiveApproval(address from, uint256 tokens, address token, bytes data) public; 256 | } 257 | 258 | contract BridgeableToken is MintableToken { 259 | function approveAndCall(address spender, uint tokens, bytes data) public returns (bool) { 260 | allowed[msg.sender][spender] = tokens; 261 | Approval(msg.sender, spender, tokens); 262 | ApproveAndCallFallBack(spender).receiveApproval(msg.sender, tokens, this, data); 263 | return true; 264 | } 265 | } 266 | 267 | // For test purposes 268 | contract Token is BridgeableToken { 269 | 270 | function Token() { 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /contracts/README.md: -------------------------------------------------------------------------------- 1 | These contracts are outdated and used for integration testing (so far) only. 2 | 3 | Please use the bridge with contracts located in [POA Bridge Smart Contracts](https://github.com/poanetwork/poa-bridge-contracts) 4 | -------------------------------------------------------------------------------- /examples/config.toml: -------------------------------------------------------------------------------- 1 | keystore = "/path/to/keystore" 2 | 3 | [home] 4 | account = "0x547aff14210b6a72e106e27f34c626762f3c4761" 5 | password = "home_password.txt" 6 | rpc_host = "http://rpc.host.for.home" 7 | rpc_port = 8545 8 | required_confirmations = 0 9 | poll_interval = 5 10 | request_timeout = 60 11 | default_gas_price = 1_000_000_000 # 1 GWEI 12 | 13 | [foreign] 14 | account = "0x006e27b6a72e1f34c626762f3c4761547aff1421" 15 | password = "foreign_password.txt" 16 | rpc_host = "https://rpc.host.for.foreign" 17 | rpc_port = 443 18 | required_confirmations = 8 19 | poll_interval = 15 20 | request_timeout = 60 21 | gas_price_oracle_url = "https://gasprice.poa.network" 22 | gas_price_speed = "instant" 23 | gas_price_timeout = 10 24 | default_gas_price = 10_000_000_000 # 10 GWEI 25 | 26 | [authorities] 27 | # Keep this section empty 28 | 29 | [transactions] 30 | withdraw_relay = { gas = 300000 } 31 | withdraw_confirm = { gas = 300000 } 32 | deposit_relay = { gas = 300000 } 33 | -------------------------------------------------------------------------------- /examples/supervisor: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | while true; do 4 | "$@" 5 | if [ $? == 3 ]; then 6 | # shutdown requested by user 7 | echo "Shutting down" 8 | exit 0 9 | fi 10 | echo "Restarting after 1 second" 11 | sleep 1 12 | done 13 | -------------------------------------------------------------------------------- /integration-tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "integration-tests" 3 | version = "0.1.0" 4 | authors = ["snd "] 5 | 6 | [dependencies] 7 | bridge = { path = "../bridge", features = ["deploy"] } 8 | futures = "0.1" 9 | jsonrpc-core = "8.0" 10 | web3 = "0.3" 11 | serde_json = "1.0" 12 | pretty_assertions = "0.2.1" 13 | tempdir = "0.3.5" 14 | tokio-core = "0.1.8" 15 | ethabi = "5.1" 16 | ethabi-contract = "5.0" 17 | ethabi-derive = "5.0" 18 | rustc-hex = "1.0" 19 | -------------------------------------------------------------------------------- /integration-tests/bridge_config.toml: -------------------------------------------------------------------------------- 1 | keystore = "keys" 2 | estimated_gas_cost_of_withdraw = 0 3 | 4 | [home] 5 | account = "0x00bd138abd70e2f00903268f3db08f2d25677c9e" 6 | required_confirmations = 0 7 | rpc_host = "http://127.0.0.1" 8 | rpc_port = 8550 9 | password = "password.txt" 10 | default_gas_price = 0 11 | 12 | [home.contract] 13 | bin = "../compiled_contracts/HomeBridge.bin" 14 | 15 | [foreign] 16 | account = "0x00bd138abd70e2f00903268f3db08f2d25677c9e" 17 | required_confirmations = 0 18 | rpc_host = "http://127.0.0.1" 19 | rpc_port = 8551 20 | password = "password.txt" 21 | default_gas_price = 0 22 | 23 | [foreign.contract] 24 | bin = "../compiled_contracts/ForeignBridge.bin" 25 | 26 | [authorities] 27 | accounts = [ 28 | "0x00bd138abd70e2f00903268f3db08f2d25677c9e", 29 | "0x00a329c0648769a73afac7f9381e08fb43dbea72" 30 | ] 31 | required_signatures = 1 32 | 33 | [transactions] 34 | home_deploy = { gas = 3000000 } 35 | foreign_deploy = { gas = 3000000 } 36 | deposit_relay = { gas = 3000000 } 37 | withdraw_relay = { gas = 3000000 } 38 | withdraw_confirm = { gas = 3000000 } 39 | -------------------------------------------------------------------------------- /integration-tests/bridge_config_gas_price.toml: -------------------------------------------------------------------------------- 1 | keystore = "keys" 2 | estimated_gas_cost_of_withdraw = 0 3 | 4 | [home] 5 | account = "0x00bd138abd70e2f00903268f3db08f2d25677c9e" 6 | required_confirmations = 0 7 | rpc_host = "http://127.0.0.1" 8 | rpc_port = 8550 9 | password = "password.txt" 10 | 11 | [home.contract] 12 | bin = "../compiled_contracts/HomeBridge.bin" 13 | 14 | [foreign] 15 | account = "0x00bd138abd70e2f00903268f3db08f2d25677c9e" 16 | required_confirmations = 0 17 | rpc_host = "http://127.0.0.1" 18 | rpc_port = 8551 19 | password = "password.txt" 20 | 21 | [foreign.contract] 22 | bin = "../compiled_contracts/ForeignBridge.bin" 23 | 24 | [authorities] 25 | accounts = [ 26 | "0x00bd138abd70e2f00903268f3db08f2d25677c9e", 27 | "0x00a329c0648769a73afac7f9381e08fb43dbea72" 28 | ] 29 | required_signatures = 1 30 | 31 | [transactions] 32 | home_deploy = { gas = 3000000, gas_price = 1 } 33 | foreign_deploy = { gas = 3000000, gas_price = 1 } 34 | deposit_relay = { gas = 3000000, gas_price = 1 } 35 | withdraw_relay = { gas = 3000000, gas_price = 1 } 36 | withdraw_confirm = { gas = 3000000, gas_price = 1 } 37 | -------------------------------------------------------------------------------- /integration-tests/build.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | fn main() { 4 | // rerun build script if bridge contract has changed. 5 | // without this cargo doesn't since the bridge contract 6 | // is outside the crate directories 7 | println!("cargo:rerun-if-changed=../contracts/BridgrableToken.sol"); 8 | 9 | match Command::new("solc") 10 | .arg("--abi") 11 | .arg("--bin") 12 | .arg("--optimize") 13 | .arg("--output-dir").arg("../compiled_contracts") 14 | .arg("--overwrite") 15 | .arg("../contracts/BridgeableToken.sol") 16 | .status() 17 | { 18 | Ok(exit_status) => { 19 | if !exit_status.success() { 20 | if let Some(code) = exit_status.code() { 21 | panic!("`solc` exited with error exit status code `{}`", code); 22 | } else { 23 | panic!("`solc` exited because it was terminated by a signal"); 24 | } 25 | } 26 | }, 27 | Err(err) => { 28 | if let std::io::ErrorKind::NotFound = err.kind() { 29 | panic!("`solc` executable not found in `$PATH`. `solc` is required to compile the bridge contracts. please install it: https://solidity.readthedocs.io/en/develop/installing-solidity.html"); 30 | } else { 31 | panic!("an error occurred when trying to spawn `solc`: {}", err); 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /integration-tests/keys/authority.json: -------------------------------------------------------------------------------- 1 | {"id":"787b7cf7-9c21-1731-29c5-bce30d366469","version":3,"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"18bead05af3d7aba23541358038584dc"},"ciphertext":"61de22e3a1b85b5805bfe3c9287e151fa383933cde96d87d55aaa6248802e32a","kdf":"pbkdf2","kdfparams":{"c":10240,"dklen":32,"prf":"hmac-sha256","salt":"b955d471c36edba9f23f9f02dabc6d28ebd42c3b439b82f46a8040b03cd3db7a"},"mac":"cf7eb554dbdfd8ca641eb2e54a12e5bf67333357829103e67cc23124c7bd8929"},"address":"00bd138abd70e2f00903268f3db08f2d25677c9e","name":"","meta":"{}"} -------------------------------------------------------------------------------- /integration-tests/keys/user.json: -------------------------------------------------------------------------------- 1 | {"id":"e9cce5c6-5611-8543-f1fa-8e4a510a7136","version":3,"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"b3b2fc9fed2959bae7d03346b0e60adb"},"ciphertext":"deba44f0402473cdc65c876ddc09378fc87c69e44d7b5a11023cc2a40eaf11fb","kdf":"pbkdf2","kdfparams":{"c":10240,"dklen":32,"prf":"hmac-sha256","salt":"4d8ad132b69933be5fb6ae90ac3ee4eae29b9334c346119c6be36a49538bd431"},"mac":"c41a0bd97b118c398cd871a5e3f77cc86967145f2ebabc84a95465cceb5865f6"},"address":"00a329c0648769a73afac7f9381e08fb43dbea72","name":"Development Account","meta":"{\"description\":\"Never use this account outside of develoopment chain!\",\"passwordHint\":\"Password is empty string\"}"} -------------------------------------------------------------------------------- /integration-tests/password.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /res/deposit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omni/poa-bridge/924e0ad79c216eac139b600d466d39c9f530dcda/res/deposit.png -------------------------------------------------------------------------------- /res/withdraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omni/poa-bridge/924e0ad79c216eac139b600d466d39c9f530dcda/res/withdraw.png -------------------------------------------------------------------------------- /tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tests" 3 | version = "0.1.0" 4 | authors = ["debris "] 5 | 6 | [dependencies] 7 | bridge = { path = "../bridge" } 8 | futures = "0.1" 9 | jsonrpc-core = "8.0" 10 | web3 = "0.3" 11 | serde_json = "1.0" 12 | pretty_assertions = "0.2.1" 13 | ethabi = "5.0" 14 | ethcore = { git = "http://github.com/paritytech/parity", rev = "991f0ca" } 15 | ethereum-types = "0.3" 16 | rustc-hex = "1.0" 17 | -------------------------------------------------------------------------------- /tests/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate serde_json; 2 | extern crate futures; 3 | extern crate jsonrpc_core as rpc; 4 | extern crate web3; 5 | extern crate bridge; 6 | #[macro_use] 7 | extern crate pretty_assertions; 8 | extern crate ethcore; 9 | 10 | use std::cell::Cell; 11 | use web3::Transport; 12 | 13 | #[derive(Debug, Clone)] 14 | pub struct MockedRequest { 15 | pub method: String, 16 | pub params: Vec, 17 | } 18 | 19 | impl From<(&'static str, serde_json::Value)> for MockedRequest { 20 | fn from(a: (&'static str, serde_json::Value)) -> Self { 21 | MockedRequest { 22 | method: a.0.to_owned(), 23 | params: a.1.as_array().unwrap().clone() 24 | } 25 | } 26 | } 27 | 28 | #[derive(Debug, Clone)] 29 | pub struct MockedTransport { 30 | pub requests: Cell, 31 | pub expected_requests: Vec, 32 | pub mocked_responses: Vec, 33 | } 34 | 35 | impl Transport for MockedTransport { 36 | type Out = web3::Result; 37 | 38 | fn prepare(&self, method: &str, params: Vec) -> (usize, rpc::Call) { 39 | let n = self.requests.get(); 40 | assert_eq!(&self.expected_requests[n].method as &str, method, "invalid method called"); 41 | 42 | for (expected_params, params) in self.expected_requests[n].params.iter().zip(params.iter()) { 43 | assert_eq!(expected_params.get("address"), params.get("address"), "invalid method params, addresses do not match"); 44 | assert_eq!(expected_params.get("fromBlock"), params.get("fromBlock"), "invalid method params, from-blocks do not match"); 45 | assert_eq!(expected_params.get("limit"), params.get("limit"), "invalid method params, limits do not match"); 46 | assert_eq!(expected_params.get("toBlock"), params.get("toBlock"), "invalid method params, to-blocks do not match"); 47 | 48 | let expected_topics: Vec = if let Some(ref topics) = expected_params.get("topics") { 49 | topics.as_array().unwrap().clone() 50 | .iter() 51 | .filter_map(|topic| 52 | if topic != &rpc::Value::Null { 53 | Some(topic.clone()) 54 | } else { 55 | None 56 | } 57 | ) 58 | .collect() 59 | } else { 60 | vec![] 61 | }; 62 | 63 | let topics: Vec = if let Some(ref topics) = params.get("topics") { 64 | topics.as_array().unwrap().clone() 65 | .iter() 66 | .filter_map(|topic| 67 | if topic != &rpc::Value::Null { 68 | Some(topic.clone()) 69 | } else { 70 | None 71 | } 72 | ) 73 | .collect() 74 | } else { 75 | vec![] 76 | }; 77 | 78 | assert_eq!(expected_topics, topics, "invalid method params, topics do not match"); 79 | } 80 | self.requests.set(n + 1); 81 | 82 | let request = web3::helpers::build_request(1, method, params); 83 | (n + 1, request) 84 | } 85 | 86 | fn send(&self, _id: usize, _request: rpc::Call) -> web3::Result { 87 | let response = self.mocked_responses.iter().nth(self.requests.get() - 1).expect("missing response"); 88 | let f = futures::finished(response.clone()); 89 | Box::new(f) 90 | } 91 | } 92 | 93 | #[macro_export] 94 | macro_rules! test_transport_stream { 95 | ( 96 | name => $name: ident, 97 | init => $init_stream: expr, 98 | expected => $expected: expr, 99 | $($method: expr => req => $req: expr, res => $res: expr ;)* 100 | ) => { 101 | #[test] 102 | fn $name() { 103 | use self::futures::{Future, Stream}; 104 | 105 | let transport = $crate::MockedTransport { 106 | requests: Default::default(), 107 | expected_requests: vec![$($method),*].into_iter().zip(vec![$($req),*].into_iter()).map(Into::into).collect(), 108 | mocked_responses: vec![$($res),*], 109 | }; 110 | let stream = $init_stream(&transport); 111 | let res = stream.collect().wait(); 112 | assert_eq!($expected, res.unwrap()); 113 | } 114 | } 115 | } 116 | 117 | #[macro_export] 118 | macro_rules! test_app_stream { 119 | ( 120 | name => $name: ident, 121 | database => $db: expr, 122 | home => account => $home_acc: expr, confirmations => $home_conf: expr; 123 | foreign => account => $foreign_acc: expr, confirmations => $foreign_conf: expr; 124 | authorities => accounts => $authorities_accs: expr, signatures => $signatures: expr; 125 | txs => $txs: expr, 126 | init => $init_stream: expr, 127 | expected => $expected: expr, 128 | home_transport => [$($home_method: expr => req => $home_req: expr, res => $home_res: expr ;)*], 129 | foreign_transport => [$($foreign_method: expr => req => $foreign_req: expr, res => $foreign_res: expr ;)*] 130 | ) => { 131 | #[test] 132 | #[allow(unused_imports)] 133 | fn $name() { 134 | use self::std::sync::Arc; 135 | use self::std::sync::atomic::AtomicBool; 136 | use self::std::time::Duration; 137 | use self::futures::{Future, Stream}; 138 | use self::bridge::app::{App, Connections}; 139 | use self::bridge::contracts::{foreign, home}; 140 | use self::bridge::config::{Config, Authorities, Node, NodeInfo, ContractConfig, Transactions, TransactionConfig, GasPriceSpeed}; 141 | use self::bridge::database::Database; 142 | use ethcore::account_provider::AccountProvider; 143 | 144 | let home = $crate::MockedTransport { 145 | requests: Default::default(), 146 | expected_requests: vec![$($home_method),*].into_iter().zip(vec![$($home_req),*].into_iter()).map(Into::into).collect(), 147 | mocked_responses: vec![$($home_res),*], 148 | }; 149 | 150 | let foreign = $crate::MockedTransport { 151 | requests: Default::default(), 152 | expected_requests: vec![$($foreign_method),*].into_iter().zip(vec![$($foreign_req),*].into_iter()).map(Into::into).collect(), 153 | mocked_responses: vec![$($foreign_res),*], 154 | }; 155 | 156 | let config = Config { 157 | txs: $txs, 158 | home: Node { 159 | account: $home_acc.parse().unwrap(), 160 | contract: ContractConfig { 161 | bin: Default::default(), 162 | }, 163 | poll_interval: Duration::from_secs(0), 164 | request_timeout: Duration::from_secs(5), 165 | required_confirmations: $home_conf, 166 | rpc_host: "".into(), 167 | rpc_port: 8545, 168 | password: "password.txt".into(), 169 | info: NodeInfo::default(), 170 | gas_price_oracle_url: None, 171 | gas_price_speed: GasPriceSpeed::Fast, 172 | gas_price_timeout: Duration::from_secs(5), 173 | default_gas_price: 0, 174 | }, 175 | foreign: Node { 176 | account: $foreign_acc.parse().unwrap(), 177 | contract: ContractConfig { 178 | bin: Default::default(), 179 | }, 180 | poll_interval: Duration::from_secs(0), 181 | request_timeout: Duration::from_secs(5), 182 | required_confirmations: $foreign_conf, 183 | rpc_host: "".into(), 184 | rpc_port: 8545, 185 | password: "password.txt".into(), 186 | info: NodeInfo::default(), 187 | gas_price_oracle_url: None, 188 | gas_price_speed: GasPriceSpeed::Fast, 189 | gas_price_timeout: Duration::from_secs(5), 190 | default_gas_price: 0, 191 | }, 192 | authorities: Authorities { 193 | accounts: $authorities_accs.iter().map(|a: &&str| a.parse().unwrap()).collect(), 194 | required_signatures: $signatures, 195 | }, 196 | estimated_gas_cost_of_withdraw: 100_000, 197 | keystore: "/keys/".into(), 198 | }; 199 | 200 | let app = App { 201 | config, 202 | database_path: "".into(), 203 | connections: Connections { 204 | home: &home, 205 | foreign: &foreign, 206 | }, 207 | home_bridge: home::HomeBridge::default(), 208 | foreign_bridge: foreign::ForeignBridge::default(), 209 | timer: Default::default(), 210 | running: Arc::new(AtomicBool::new(true)), 211 | keystore: AccountProvider::transient_provider(), 212 | }; 213 | 214 | let app = Arc::new(app); 215 | let stream = $init_stream(app, &$db); 216 | let res = stream.collect().wait(); 217 | 218 | assert_eq!($expected, res.unwrap()); 219 | 220 | assert_eq!( 221 | home.expected_requests.len(), 222 | home.requests.get(), 223 | "home: expected {} requests but received only {}", 224 | home.expected_requests.len(), 225 | home.requests.get() 226 | ); 227 | 228 | assert_eq!( 229 | foreign.expected_requests.len(), 230 | foreign.requests.get(), 231 | "foreign: expected {} requests but received only {}", 232 | foreign.expected_requests.len(), 233 | foreign.requests.get() 234 | ); 235 | } 236 | } 237 | } 238 | 239 | #[cfg(test)] 240 | mod tests { 241 | } 242 | -------------------------------------------------------------------------------- /tests/tests/deposit_relay.rs: -------------------------------------------------------------------------------- 1 | extern crate futures; 2 | #[macro_use] 3 | extern crate serde_json; 4 | extern crate bridge; 5 | #[macro_use] 6 | extern crate tests; 7 | extern crate ethcore; 8 | 9 | use bridge::bridge::create_deposit_relay; 10 | 11 | const DEPOSIT_TOPIC: &str = "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c"; 12 | 13 | use std::sync::RwLock; 14 | 15 | test_app_stream! { 16 | name => deposit_relay_basic, 17 | database => Database::default(), 18 | home => 19 | account => "0000000000000000000000000000000000000001", 20 | confirmations => 12; 21 | foreign => 22 | account => "0000000000000000000000000000000000000001", 23 | confirmations => 12; 24 | authorities => 25 | accounts => [ 26 | "0000000000000000000000000000000000000001", 27 | "0000000000000000000000000000000000000002", 28 | ], 29 | signatures => 1; 30 | txs => Transactions::default(), 31 | init => |app, db| create_deposit_relay(app, db, Arc::new(RwLock::new(Some(99999999999u64.into()))), 17, Arc::new(RwLock::new(1))).take(2), 32 | expected => vec![0x1005, 0x1006], 33 | home_transport => [ 34 | "eth_blockNumber" => 35 | req => json!([]), 36 | res => json!("0x1011"); 37 | "eth_getLogs" => 38 | req => json!([{ 39 | "address": ["0x0000000000000000000000000000000000000000"], 40 | "fromBlock": "0x1", 41 | "limit": null, 42 | "toBlock": "0x1005", 43 | "topics": [[DEPOSIT_TOPIC], null, null, null] 44 | }]), 45 | res => json!([]); 46 | "eth_blockNumber" => 47 | req => json!([]), 48 | res => json!("0x1012"); 49 | "eth_getLogs" => 50 | req => json!([{ 51 | "address": ["0x0000000000000000000000000000000000000000"], 52 | "fromBlock": "0x1006", 53 | "limit": null, 54 | "toBlock": "0x1006", 55 | "topics": [[DEPOSIT_TOPIC], null, null, null] 56 | }]), 57 | res => json!([]); 58 | ], 59 | foreign_transport => [] 60 | } 61 | 62 | test_app_stream! { 63 | name => deposit_relay_single_log, 64 | database => Database { 65 | checked_deposit_relay: 5, 66 | ..Default::default() 67 | }, 68 | home => 69 | account => "0000000000000000000000000000000000000001", 70 | confirmations => 12; 71 | foreign => 72 | account => "0000000000000000000000000000000000000001", 73 | confirmations => 12; 74 | authorities => 75 | accounts => [ 76 | "0000000000000000000000000000000000000001", 77 | "0000000000000000000000000000000000000002", 78 | ], 79 | signatures => 1; 80 | txs => Transactions::default(), 81 | init => |app, db| create_deposit_relay(app, db, Arc::new(RwLock::new(Some(99999999999u64.into()))), 17, Arc::new(RwLock::new(1))).take(2), 82 | expected => vec![0x1005, 0x1006], 83 | home_transport => [ 84 | "eth_blockNumber" => 85 | req => json!([]), 86 | res => json!("0x1011"); 87 | "eth_getLogs" => 88 | req => json!([{ 89 | "address": ["0x0000000000000000000000000000000000000000"], 90 | "fromBlock": "0x6", 91 | "limit": null, 92 | "toBlock":"0x1005", 93 | "topics": [[DEPOSIT_TOPIC], null, null, null] 94 | }]), 95 | res => json!([{ 96 | "address": "0x0000000000000000000000000000000000000000", 97 | "topics": [DEPOSIT_TOPIC], 98 | "data": "0x000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebcccc00000000000000000000000000000000000000000000000000000000000000f0", 99 | "type": "", 100 | "transactionHash": "0x884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a9424364" 101 | }]); 102 | "eth_blockNumber" => 103 | req => json!([]), 104 | res => json!("0x1012"); 105 | "eth_getLogs" => 106 | req => json!([{ 107 | "address": ["0x0000000000000000000000000000000000000000"], 108 | "fromBlock": "0x1006", 109 | "limit": null, 110 | "toBlock": "0x1006", 111 | "topics":[[DEPOSIT_TOPIC], null, null, null] 112 | }]), 113 | res => json!([]); 114 | ], 115 | foreign_transport => [ 116 | "eth_sendTransaction" => 117 | req => json!([{ 118 | "data": "0x26b3293f000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebcccc00000000000000000000000000000000000000000000000000000000000000f0884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a9424364", 119 | "from": "0x0000000000000000000000000000000000000001", 120 | "gas": "0x0", 121 | "gasPrice": "0x0", 122 | "to": "0x0000000000000000000000000000000000000000" 123 | }]), 124 | res => json!("0x1db8f385535c0d178b8f40016048f3a3cffee8f94e68978ea4b277f57b638f0b"); 125 | ] 126 | } 127 | 128 | test_app_stream! { 129 | name => deposit_relay_check_gas, 130 | database => Database { 131 | checked_deposit_relay: 5, 132 | ..Default::default() 133 | }, 134 | home => 135 | account => "0000000000000000000000000000000000000001", 136 | confirmations => 12; 137 | foreign => 138 | account => "0000000000000000000000000000000000000001", 139 | confirmations => 12; 140 | authorities => 141 | accounts => [ 142 | "0000000000000000000000000000000000000001", 143 | "0000000000000000000000000000000000000002", 144 | ], 145 | signatures => 1; 146 | txs => Transactions { 147 | deposit_relay: TransactionConfig { 148 | gas: 0xfd, 149 | gas_price: 0xa0, 150 | concurrency: 100, 151 | }, 152 | ..Default::default() 153 | }, 154 | init => |app, db| create_deposit_relay(app, db, Arc::new(RwLock::new(Some(99999999999u64.into()))), 17, Arc::new(RwLock::new(1))).take(1), 155 | expected => vec![0x1005], 156 | home_transport => [ 157 | "eth_blockNumber" => 158 | req => json!([]), 159 | res => json!("0x1011"); 160 | "eth_getLogs" => 161 | req => json!([{ 162 | "address": ["0x0000000000000000000000000000000000000000"], 163 | "fromBlock": "0x6", 164 | "limit": null, 165 | "toBlock": "0x1005", 166 | "topics": [[DEPOSIT_TOPIC], null, null, null] 167 | }]), 168 | res => json!([{ 169 | "address": "0x0000000000000000000000000000000000000000", 170 | "topics": [DEPOSIT_TOPIC], 171 | "data": "0x000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebcccc00000000000000000000000000000000000000000000000000000000000000f0","type":"","transactionHash":"0x884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a9424364" 172 | }]); 173 | ], 174 | foreign_transport => [ 175 | "eth_sendTransaction" => 176 | req => json!([{ 177 | "data": "0x26b3293f000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebcccc00000000000000000000000000000000000000000000000000000000000000f0884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a9424364", 178 | "from": "0x0000000000000000000000000000000000000001", 179 | "gas": "0xfd", 180 | "gasPrice": "0xa0", 181 | "to": "0x0000000000000000000000000000000000000000" 182 | }]), 183 | res => json!("0x1db8f385535c0d178b8f40016048f3a3cffee8f94e68978ea4b277f57b638f0b"); 184 | ] 185 | } 186 | 187 | test_app_stream! { 188 | name => deposit_relay_contract_address, 189 | database => Database { 190 | home_contract_address: "0000000000000000000000000000000000000cc1".into(), 191 | foreign_contract_address: "0000000000000000000000000000000000000dd1".into(), 192 | ..Default::default() 193 | }, 194 | home => 195 | account => "0000000000000000000000000000000000000001", 196 | confirmations => 12; 197 | foreign => 198 | account => "0000000000000000000000000000000000000001", 199 | confirmations => 12; 200 | authorities => 201 | accounts => [ 202 | "0000000000000000000000000000000000000001", 203 | "0000000000000000000000000000000000000002", 204 | ], 205 | signatures => 1; 206 | txs => Transactions::default(), 207 | init => |app, db| create_deposit_relay(app, db, Arc::new(RwLock::new(Some(99999999999u64.into()))), 17, Arc::new(RwLock::new(1))).take(1), 208 | expected => vec![0x1005], 209 | home_transport => [ 210 | "eth_blockNumber" => 211 | req => json!([]), 212 | res => json!("0x1011"); 213 | "eth_getLogs" => 214 | req => json!([{ 215 | "address": ["0x0000000000000000000000000000000000000cc1"], 216 | "fromBlock": "0x1", 217 | "limit": null, 218 | "toBlock": "0x1005", 219 | "topics": [[DEPOSIT_TOPIC], null, null, null] 220 | }]), 221 | res => json!([{ 222 | "address": "0x0000000000000000000000000000000000000cc1", 223 | "topics": ["0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c"], 224 | "data": "0x000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebcccc00000000000000000000000000000000000000000000000000000000000000f0", 225 | "type": "", 226 | "transactionHash": "0x884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a9424364" 227 | }]); 228 | ], 229 | foreign_transport => [ 230 | "eth_sendTransaction" => 231 | req => json!([{ 232 | "data": "0x26b3293f000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebcccc00000000000000000000000000000000000000000000000000000000000000f0884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a9424364", 233 | "from": "0x0000000000000000000000000000000000000001", 234 | "gas": "0x0", 235 | "gasPrice": "0x0", 236 | "to": "0x0000000000000000000000000000000000000dd1" 237 | }]), 238 | res => json!("0x1db8f385535c0d178b8f40016048f3a3cffee8f94e68978ea4b277f57b638f0b"); 239 | ] 240 | } 241 | 242 | test_app_stream! { 243 | name => deposit_relay_accounts, 244 | database => Database { 245 | home_contract_address: "0000000000000000000000000000000000000cc1".into(), 246 | foreign_contract_address: "0000000000000000000000000000000000000dd1".into(), 247 | ..Default::default() 248 | }, 249 | home => 250 | account => "00000000000000000000000000000000000000ff", 251 | confirmations => 12; 252 | foreign => 253 | account => "00000000000000000000000000000000000000ee", 254 | confirmations => 12; 255 | authorities => 256 | accounts => [ 257 | "0000000000000000000000000000000000000001", 258 | "0000000000000000000000000000000000000002", 259 | ], 260 | signatures => 1; 261 | txs => Transactions::default(), 262 | init => |app, db| create_deposit_relay(app, db, Arc::new(RwLock::new(Some(99999999999u64.into()))), 17, Arc::new(RwLock::new(1))).take(1), 263 | expected => vec![0x1005], 264 | home_transport => [ 265 | "eth_blockNumber" => 266 | req => json!([]), 267 | res => json!("0x1011"); 268 | "eth_getLogs" => 269 | req => json!([{ 270 | "address": ["0x0000000000000000000000000000000000000cc1"], 271 | "fromBlock": "0x1", 272 | "limit": null, 273 | "toBlock": "0x1005", 274 | "topics": [[DEPOSIT_TOPIC], null, null, null] 275 | }]), 276 | res => json!([{ 277 | "address": "0x0000000000000000000000000000000000000cc1", 278 | "topics": [DEPOSIT_TOPIC], 279 | "data": "0x000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebcccc00000000000000000000000000000000000000000000000000000000000000f0", 280 | "type": "", 281 | "transactionHash": "0x884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a9424364" 282 | }]); 283 | ], 284 | foreign_transport => [ 285 | "eth_sendTransaction" => 286 | req => json!([{ 287 | "data": "0x26b3293f000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebcccc00000000000000000000000000000000000000000000000000000000000000f0884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a9424364", 288 | "from": "0x00000000000000000000000000000000000000ee", 289 | "gas": "0x0", 290 | "gasPrice": "0x0", 291 | "to":"0x0000000000000000000000000000000000000dd1" 292 | }]), 293 | res => json!("0x1db8f385535c0d178b8f40016048f3a3cffee8f94e68978ea4b277f57b638f0b"); 294 | ] 295 | } 296 | 297 | test_app_stream! { 298 | name => deposit_relay_multiple_logs, 299 | database => Database::default(), 300 | home => 301 | account => "0000000000000000000000000000000000000001", 302 | confirmations => 12; 303 | foreign => 304 | account => "0000000000000000000000000000000000000001", 305 | confirmations => 12; 306 | authorities => 307 | accounts => [ 308 | "0000000000000000000000000000000000000001", 309 | "0000000000000000000000000000000000000002", 310 | ], 311 | signatures => 1; 312 | txs => Transactions::default(), 313 | init => |app, db| create_deposit_relay(app, db, Arc::new(RwLock::new(Some(99999999999u64.into()))), 17, Arc::new(RwLock::new(1))).take(1), 314 | expected => vec![0x1005], 315 | home_transport => [ 316 | "eth_blockNumber" => 317 | req => json!([]), 318 | res => json!("0x1011"); 319 | "eth_getLogs" => 320 | req => json!([{ 321 | "address": ["0x0000000000000000000000000000000000000000"], 322 | "fromBlock": "0x1", 323 | "limit": null, 324 | "toBlock": "0x1005", 325 | "topics": [[DEPOSIT_TOPIC], null, null, null] 326 | }]), 327 | res => json!([ 328 | { 329 | "address": "0x0000000000000000000000000000000000000000", 330 | "topics": ["0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c"], 331 | "data": "0x000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebcccc00000000000000000000000000000000000000000000000000000000000000f0", 332 | "type": "", 333 | "transactionHash": "0x884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a9424364" 334 | }, 335 | { 336 | "address":"0x0000000000000000000000000000000000000000", 337 | "topics": ["0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c"], 338 | "data": "0x000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebcccc00000000000000000000000000000000000000000000000000000000000000f0", 339 | "type": "", 340 | "transactionHash": "0x884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a942436f" 341 | } 342 | ]); 343 | ], 344 | foreign_transport => [ 345 | "eth_sendTransaction" => 346 | req => json!([{ 347 | "data": "0x26b3293f000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebcccc00000000000000000000000000000000000000000000000000000000000000f0884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a9424364", 348 | "from": "0x0000000000000000000000000000000000000001", 349 | "gas": "0x0", 350 | "gasPrice": "0x0", 351 | "to": "0x0000000000000000000000000000000000000000" 352 | }]), 353 | res => json!("0x1db8f385535c0d178b8f40016048f3a3cffee8f94e68978ea4b277f57b638f0b"); 354 | "eth_sendTransaction" => 355 | req => json!([{ 356 | "data": "0x26b3293f000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebcccc00000000000000000000000000000000000000000000000000000000000000f0884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a942436f", 357 | "from": "0x0000000000000000000000000000000000000001", 358 | "gas": "0x0", 359 | "gasPrice": "0x0", 360 | "to": "0x0000000000000000000000000000000000000000" 361 | }]), 362 | res => json!("0x1db8f385535c0d178b8f40016048f3a3cffee8f94e68978ea4b277f57b638f0b"); 363 | ] 364 | } 365 | -------------------------------------------------------------------------------- /tests/tests/log_stream.rs: -------------------------------------------------------------------------------- 1 | extern crate futures; 2 | #[macro_use] 3 | extern crate serde_json; 4 | extern crate web3; 5 | extern crate bridge; 6 | #[macro_use] 7 | extern crate tests; 8 | extern crate ethcore; 9 | 10 | use std::time::Duration; 11 | use web3::types::{FilterBuilder, H160, H256, Log}; 12 | use bridge::api::{LogStreamInit, log_stream, LogStreamItem}; 13 | 14 | test_transport_stream! { 15 | name => log_stream_basic, 16 | init => |transport| { 17 | let init = LogStreamInit { 18 | after: 10, 19 | filter: FilterBuilder::default(), 20 | poll_interval: Duration::from_secs(0), 21 | request_timeout: Duration::from_secs(5), 22 | confirmations: 10, 23 | }; 24 | 25 | log_stream(transport, Default::default(), init).take(2) 26 | }, 27 | expected => vec![LogStreamItem { 28 | from: 0xb, 29 | to: 0x1006, 30 | logs: vec![], 31 | }, LogStreamItem { 32 | from: 0x1007, 33 | to: 0x1007, 34 | logs: vec![], 35 | }], 36 | "eth_blockNumber" => 37 | req => json!([]), 38 | res => json!("0x1010"); 39 | "eth_getLogs" => 40 | req => json!([{ 41 | "address": null, 42 | "fromBlock": "0xb", 43 | "limit": null, 44 | "toBlock": "0x1006", 45 | "topics": null 46 | }]), 47 | res => json!([]); 48 | "eth_blockNumber" => 49 | req => json!([]), 50 | res => json!("0x1010"); 51 | "eth_blockNumber" => 52 | req => json!([]), 53 | res => json!("0x1011"); 54 | "eth_getLogs" => 55 | req => json!([{ 56 | "address": null, 57 | "fromBlock": "0x1007", 58 | "limit": null, 59 | "toBlock": "0x1007", 60 | "topics": null 61 | }]), 62 | res => json!([]); 63 | } 64 | 65 | test_transport_stream! { 66 | name => log_stream_rollback, 67 | init => |transport| { 68 | let init = LogStreamInit { 69 | after: 10, 70 | filter: FilterBuilder::default(), 71 | poll_interval: Duration::from_secs(0), 72 | request_timeout: Duration::from_secs(5), 73 | confirmations: 10, 74 | }; 75 | 76 | log_stream(transport, Default::default(), init).take(2) 77 | }, 78 | expected => vec![LogStreamItem { 79 | from: 0xb, 80 | to: 0xd, 81 | logs: vec![], 82 | }, LogStreamItem { 83 | from: 0xe, 84 | to: 0xf, 85 | logs: vec![], 86 | }], 87 | "eth_blockNumber" => 88 | req => json!([]), 89 | res => json!("0x17"); 90 | "eth_getLogs" => 91 | req => json!([{ 92 | "address": null, 93 | "fromBlock": "0xb", 94 | "limit": null, 95 | "toBlock": "0xd", 96 | "topics": null 97 | }]), 98 | res => json!([]); 99 | "eth_blockNumber" => 100 | req => json!([]), 101 | res => json!("0x16"); 102 | "eth_blockNumber" => 103 | req => json!([]), 104 | res => json!("0x17"); 105 | "eth_blockNumber" => 106 | req => json!([]), 107 | res => json!("0x19"); 108 | "eth_getLogs" => 109 | req => json!([{ 110 | "address": null, 111 | "fromBlock": "0xe", 112 | "limit": null, 113 | "toBlock": "0xf", 114 | "topics": null 115 | }]), 116 | res => json!([]); 117 | } 118 | 119 | test_transport_stream! { 120 | name => log_stream_rollback_before_init, 121 | init => |transport| { 122 | let init = LogStreamInit { 123 | after: 10, 124 | filter: FilterBuilder::default(), 125 | poll_interval: Duration::from_secs(0), 126 | request_timeout: Duration::from_secs(5), 127 | confirmations: 10, 128 | }; 129 | 130 | log_stream(transport, Default::default(), init).take(1) 131 | }, 132 | expected => vec![LogStreamItem { 133 | from: 0xb, 134 | to: 0xd, 135 | logs: vec![], 136 | }], 137 | "eth_blockNumber" => 138 | req => json!([]), 139 | res => json!("0x13"); 140 | "eth_blockNumber" => 141 | req => json!([]), 142 | res => json!("0x14"); 143 | "eth_blockNumber" => 144 | req => json!([]), 145 | res => json!("0x17"); 146 | "eth_getLogs" => 147 | req => json!([{ 148 | "address": null, 149 | "fromBlock": "0xb", 150 | "limit": null, 151 | "toBlock": "0xd", 152 | "topics": null 153 | }]), 154 | res => json!([]); 155 | } 156 | 157 | test_transport_stream! { 158 | name => log_stream_zero_confirmations, 159 | init => |transport| { 160 | let init = LogStreamInit { 161 | after: 10, 162 | filter: FilterBuilder::default(), 163 | poll_interval: Duration::from_secs(0), 164 | request_timeout: Duration::from_secs(5), 165 | confirmations: 0, 166 | }; 167 | 168 | log_stream(transport, Default::default(), init).take(3) 169 | }, 170 | expected => vec![LogStreamItem { 171 | from: 0xb, 172 | to: 0x13, 173 | logs: vec![], 174 | }, LogStreamItem { 175 | from: 0x14, 176 | to: 0x14, 177 | logs: vec![], 178 | }, LogStreamItem { 179 | from: 0x15, 180 | to: 0x17, 181 | logs: vec![], 182 | }], 183 | "eth_blockNumber" => 184 | req => json!([]), 185 | res => json!("0x13"); 186 | "eth_getLogs" => 187 | req => json!([{ 188 | "address": null, 189 | "fromBlock": "0xb", 190 | "limit": null, 191 | "toBlock": "0x13", 192 | "topics": null 193 | }]), 194 | res => json!([]); 195 | "eth_blockNumber" => 196 | req => json!([]), 197 | res => json!("0x14"); 198 | "eth_getLogs" => 199 | req => json!([{ 200 | "address": null, 201 | "fromBlock": "0x14", 202 | "limit": null, 203 | "toBlock": "0x14", 204 | "topics": null 205 | }]), 206 | res => json!([]); 207 | "eth_blockNumber" => 208 | req => json!([]), 209 | res => json!("0x14"); 210 | "eth_blockNumber" => 211 | req => json!([]), 212 | res => json!("0x17"); 213 | "eth_getLogs" => 214 | req => json!([{ 215 | "address": null, 216 | "fromBlock": "0x15", 217 | "limit": null, 218 | "toBlock": "0x17", 219 | "topics": null 220 | }]), 221 | res => json!([]); 222 | } 223 | 224 | test_transport_stream! { 225 | name => log_stream_filter_with_address, 226 | init => |transport| { 227 | let init = LogStreamInit { 228 | after: 11, 229 | filter: FilterBuilder::default().address(vec![H160([0x11u8; 20])]), 230 | poll_interval: Duration::from_secs(0), 231 | request_timeout: Duration::from_secs(5), 232 | confirmations: 0, 233 | }; 234 | 235 | log_stream(transport, Default::default(), init).take(2) 236 | }, 237 | expected => vec![LogStreamItem { 238 | from: 0xc, 239 | to: 0x13, 240 | logs: vec![], 241 | }, LogStreamItem { 242 | from: 0x14, 243 | to: 0x14, 244 | logs: vec![], 245 | }], 246 | "eth_blockNumber" => 247 | req => json!([]), 248 | res => json!("0x13"); 249 | "eth_getLogs" => 250 | req => json!([{ 251 | "address": ["0x1111111111111111111111111111111111111111"], 252 | "fromBlock": "0xc", 253 | "limit": null, 254 | "toBlock": "0x13", 255 | "topics": null 256 | }]), 257 | res => json!([]); 258 | "eth_blockNumber" => 259 | req => json!([]), 260 | res => json!("0x14"); 261 | "eth_getLogs" => 262 | req => json!([{ 263 | "address":["0x1111111111111111111111111111111111111111"], 264 | "fromBlock": "0x14", 265 | "limit": null, 266 | "toBlock": "0x14", 267 | "topics": null 268 | }]), 269 | res => json!([]); 270 | } 271 | 272 | test_transport_stream! { 273 | name => log_stream_filter_with_topics, 274 | init => |transport| { 275 | let init = LogStreamInit { 276 | after: 11, 277 | filter: FilterBuilder::default().topics(Some(vec![H256([0x22; 32])]), None, None, None), 278 | poll_interval: Duration::from_secs(0), 279 | request_timeout: Duration::from_secs(5), 280 | confirmations: 0, 281 | }; 282 | 283 | log_stream(transport, Default::default(), init).take(2) 284 | }, 285 | expected => vec![LogStreamItem { 286 | from: 0xc, 287 | to: 0x13, 288 | logs: vec![], 289 | }, LogStreamItem { 290 | from: 0x14, 291 | to: 0x14, 292 | logs: vec![], 293 | }], 294 | "eth_blockNumber" => 295 | req => json!([]), 296 | res => json!("0x13"); 297 | "eth_getLogs" => 298 | req => json!([{ 299 | "address": null, 300 | "fromBlock": "0xc", 301 | "limit": null, 302 | "toBlock": "0x13", 303 | "topics":[["0x2222222222222222222222222222222222222222222222222222222222222222"], null, null, null] 304 | }]), 305 | res => json!([]); 306 | "eth_blockNumber" => 307 | req => json!([]), 308 | res => json!("0x14"); 309 | "eth_getLogs" => 310 | req => json!([{ 311 | "address": null, 312 | "fromBlock": "0x14", 313 | "limit": null, 314 | "toBlock": "0x14", 315 | "topics": [["0x2222222222222222222222222222222222222222222222222222222222222222"], null, null, null] 316 | }]), 317 | res => json!([]); 318 | } 319 | 320 | test_transport_stream! { 321 | name => log_stream_get_log, 322 | init => |transport| { 323 | let init = LogStreamInit { 324 | after: 10, 325 | filter: FilterBuilder::default(), 326 | poll_interval: Duration::from_secs(0), 327 | request_timeout: Duration::from_secs(5), 328 | confirmations: 10, 329 | }; 330 | 331 | log_stream(transport, Default::default(), init).take(1) 332 | }, 333 | expected => vec![LogStreamItem { 334 | from: 0xb, 335 | to: 0x1006, 336 | logs: vec![Log { 337 | address: "0000000000000000000000000000000000000001".into(), 338 | topics: vec![], 339 | data: vec![0x10].into(), 340 | block_hash: None, 341 | block_number: None, 342 | transaction_hash: None, 343 | transaction_index: None, 344 | log_index: None, 345 | transaction_log_index: None, 346 | log_type: None, 347 | removed: None, 348 | }], 349 | }], 350 | "eth_blockNumber" => 351 | req => json!([]), 352 | res => json!("0x1010"); 353 | "eth_getLogs" => 354 | req => json!([{ 355 | "address": null, 356 | "fromBlock": "0xb", 357 | "limit": null, 358 | "toBlock": "0x1006", 359 | "topics": null 360 | }]), 361 | res => json!([{ 362 | "address": "0x0000000000000000000000000000000000000001", 363 | "topics": [], 364 | "data": "0x10", 365 | "type": "" 366 | }]); 367 | } 368 | 369 | test_transport_stream! { 370 | name => log_stream_get_multiple_logs, 371 | init => |transport| { 372 | let init = LogStreamInit { 373 | after: 10, 374 | filter: FilterBuilder::default(), 375 | poll_interval: Duration::from_secs(0), 376 | request_timeout: Duration::from_secs(5), 377 | confirmations: 10, 378 | }; 379 | 380 | log_stream(transport, Default::default(), init).take(3) 381 | }, 382 | expected => vec![LogStreamItem { 383 | from: 0xb, 384 | to: 0x1006, 385 | logs: vec![Log { 386 | address: "0000000000000000000000000000000000000001".into(), 387 | topics: vec![], 388 | data: vec![0x10].into(), 389 | block_hash: None, 390 | block_number: None, 391 | transaction_hash: None, 392 | transaction_index: None, 393 | log_index: None, 394 | transaction_log_index: None, 395 | log_type: None, 396 | removed: None, 397 | }], 398 | }, LogStreamItem { 399 | from: 0x1007, 400 | to: 0x1007, 401 | logs: vec![], 402 | }, LogStreamItem { 403 | from: 0x1008, 404 | to: 0x1008, 405 | logs: vec![Log { 406 | address: "0000000000000000000000000000000000000002".into(), 407 | topics: vec![], 408 | data: vec![0x20].into(), 409 | block_hash: None, 410 | block_number: None, 411 | transaction_hash: None, 412 | transaction_index: None, 413 | log_index: None, 414 | transaction_log_index: None, 415 | log_type: None, 416 | removed: None, 417 | }, Log { 418 | address: "0000000000000000000000000000000000000002".into(), 419 | topics: vec![], 420 | data: vec![0x30].into(), 421 | block_hash: None, 422 | block_number: None, 423 | transaction_hash: None, 424 | transaction_index: None, 425 | log_index: None, 426 | transaction_log_index: None, 427 | log_type: None, 428 | removed: None, 429 | }], 430 | }], 431 | "eth_blockNumber" => 432 | req => json!([]), 433 | res => json!("0x1010"); 434 | "eth_getLogs" => 435 | req => json!([{ 436 | "address": null, 437 | "fromBlock": "0xb", 438 | "limit": null, 439 | "toBlock": "0x1006", 440 | "topics": null 441 | }]), 442 | res => json!([{ 443 | "address": "0x0000000000000000000000000000000000000001", 444 | "topics": [], 445 | "data": "0x10", 446 | "type": "" 447 | }]); 448 | "eth_blockNumber" => 449 | req => json!([]), 450 | res => json!("0x1011"); 451 | "eth_getLogs" => 452 | req => json!([{ 453 | "address": null, 454 | "fromBlock": "0x1007", 455 | "limit": null, 456 | "toBlock": "0x1007", 457 | "topics": null 458 | }]), 459 | res => json!([]); 460 | "eth_blockNumber" => 461 | req => json!([]), 462 | res => json!("0x1012"); 463 | "eth_getLogs" => 464 | req => json!([{ 465 | "address": null, 466 | "fromBlock": "0x1008", 467 | "limit": null, 468 | "toBlock": "0x1008", 469 | "topics": null 470 | }]), 471 | res => json!([ 472 | { 473 | "address": "0x0000000000000000000000000000000000000002", 474 | "topics": [], 475 | "data": "0x20", 476 | "type":"" 477 | }, 478 | { 479 | "address":"0x0000000000000000000000000000000000000002", 480 | "topics": [], 481 | "data": "0x30", 482 | "type": "" 483 | } 484 | ]); 485 | } 486 | -------------------------------------------------------------------------------- /tests/tests/withdraw_relay.rs: -------------------------------------------------------------------------------- 1 | /// test interactions of withdraw_relay state machine with RPC 2 | 3 | extern crate futures; 4 | #[macro_use] 5 | extern crate serde_json; 6 | extern crate bridge; 7 | #[macro_use] 8 | extern crate tests; 9 | extern crate ethabi; 10 | extern crate ethereum_types; 11 | extern crate rustc_hex; 12 | extern crate ethcore; 13 | 14 | use ethereum_types::{U256, H256}; 15 | use rustc_hex::ToHex; 16 | 17 | use bridge::bridge::create_withdraw_relay; 18 | use bridge::message_to_mainnet::MessageToMainnet; 19 | use bridge::signature::Signature; 20 | use bridge::contracts; 21 | 22 | use std::sync::RwLock; 23 | 24 | const COLLECTED_SIGNATURES_TOPIC: &str = "0xeb043d149eedb81369bec43d4c3a3a53087debc88d2525f13bfaa3eecda28b5c"; 25 | 26 | // 1 signature required. relay polled twice. 27 | // no CollectedSignatures on ForeignBridge. 28 | // no relay. 29 | test_app_stream! { 30 | name => withdraw_relay_no_log_no_relay, 31 | database => Database::default(), 32 | home => 33 | account => "0000000000000000000000000000000000000001", 34 | confirmations => 12; 35 | foreign => 36 | account => "0000000000000000000000000000000000000001", 37 | confirmations => 12; 38 | authorities => 39 | accounts => [ 40 | "0000000000000000000000000000000000000001", 41 | "0000000000000000000000000000000000000002", 42 | ], 43 | signatures => 1; 44 | txs => Transactions::default(), 45 | init => |app, db| create_withdraw_relay(app, db, Arc::new(RwLock::new(Some(99999999999u64.into()))), 17, Arc::new(RwLock::new(1))).take(2), 46 | expected => vec![0x1005, 0x1006], 47 | home_transport => [], 48 | foreign_transport => [ 49 | "eth_blockNumber" => 50 | req => json!([]), 51 | res => json!("0x1011"); 52 | "eth_getLogs" => 53 | req => json!([{ 54 | "address": ["0x0000000000000000000000000000000000000000"], 55 | "fromBlock": "0x1", 56 | "limit": null, 57 | "toBlock": "0x1005", 58 | "topics": [[COLLECTED_SIGNATURES_TOPIC], null, null, null] 59 | }]), 60 | res => json!([]); 61 | "eth_blockNumber" => 62 | req => json!([]), 63 | res => json!("0x1012"); 64 | "eth_getLogs" => 65 | req => json!([{ 66 | "address": ["0x0000000000000000000000000000000000000000"], 67 | "fromBlock": "0x1006", 68 | "limit": null, 69 | "toBlock": "0x1006", 70 | "topics": [[COLLECTED_SIGNATURES_TOPIC], null, null, null] 71 | }]), 72 | res => json!([]); 73 | ] 74 | } 75 | 76 | // 2 signatures required. relay polled twice. 77 | // single CollectedSignatures log present. message value covers relay cost. 78 | // authority not responsible. 79 | // message is ignored. 80 | test_app_stream! { 81 | name => withdraw_relay_single_log_authority_not_responsible_no_relay, 82 | database => Database::default(), 83 | home => 84 | account => "0000000000000000000000000000000000000001", 85 | confirmations => 12; 86 | foreign => 87 | account => "0000000000000000000000000000000000000001", 88 | confirmations => 12; 89 | authorities => 90 | accounts => [ 91 | "0000000000000000000000000000000000000001", 92 | "0000000000000000000000000000000000000002", 93 | ], 94 | signatures => 1; 95 | txs => Transactions::default(), 96 | init => |app, db| create_withdraw_relay(app, db, Arc::new(RwLock::new(Some(99999999999u64.into()))), 17, Arc::new(RwLock::new(1))).take(1), 97 | expected => vec![0x1005], 98 | home_transport => [], 99 | foreign_transport => [ 100 | "eth_blockNumber" => 101 | req => json!([]), 102 | res => json!("0x1011"); 103 | "eth_getLogs" => 104 | req => json!([{ 105 | "address": ["0x0000000000000000000000000000000000000000"], 106 | "fromBlock": "0x1", 107 | "limit": null, 108 | "toBlock": "0x1005", 109 | "topics": [[COLLECTED_SIGNATURES_TOPIC], null, null, null] 110 | }]), 111 | res => json!([{ 112 | "address": "0x0000000000000000000000000000000000000000", 113 | "topics": [COLLECTED_SIGNATURES_TOPIC], 114 | "data": "0x000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebcccc00000000000000000000000000000000000000000000000000000000000000f0", 115 | "type": "", 116 | "transactionHash": "0x884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a9424364" 117 | }]); 118 | ] 119 | } 120 | 121 | // 2 signatures required. relay polled twice. 122 | // single CollectedSignatures log present. 123 | // message gets relayed. 124 | test_app_stream! { 125 | name => withdraw_relay_single_log_sufficient_value_relay, 126 | database => Database { 127 | home_contract_address: "00000000000000000000000000000000000000dd".into(), 128 | foreign_contract_address: "00000000000000000000000000000000000000ee".into(), 129 | ..Default::default() 130 | }, 131 | home => 132 | account => "0000000000000000000000000000000000000001", 133 | confirmations => 12; 134 | foreign => 135 | account => "aff3454fce5edbc8cca8697c15331677e6ebcccc", 136 | confirmations => 12; 137 | authorities => 138 | accounts => [ 139 | "0000000000000000000000000000000000000001", 140 | "0000000000000000000000000000000000000002", 141 | ], 142 | signatures => 2; 143 | txs => Transactions::default(), 144 | init => |app, db| create_withdraw_relay(app, db, Arc::new(RwLock::new(Some(99999999999u64.into()))), 17, Arc::new(RwLock::new(1))).take(1), 145 | expected => vec![0x1005], 146 | home_transport => [ 147 | // `HomeBridge.withdraw` 148 | "eth_sendTransaction" => 149 | req => json!([{ 150 | "data": format!("0x{}", contracts::home::HomeBridge::default() 151 | .functions() 152 | .withdraw() 153 | .input( 154 | vec![U256::from(1), U256::from(4)], 155 | vec![H256::from(2), H256::from(5)], 156 | vec![H256::from(3), H256::from(6)], 157 | MessageToMainnet { 158 | recipient: [1u8; 20].into(), 159 | value: 10000.into(), 160 | sidenet_transaction_hash: "0x884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a9424364".into(), 161 | mainnet_gas_price: 1000.into(), 162 | }.to_bytes() 163 | ).to_hex()), 164 | "from": "0x0000000000000000000000000000000000000001", 165 | "gas": "0x0", 166 | "gasPrice": "0x3e8", 167 | "to": "0x00000000000000000000000000000000000000dd" 168 | }]), 169 | res => json!("0x1db8f385535c0d178b8f40016048f3a3cffee8f94e68978ea4b277f57b638f0b"); 170 | ], 171 | foreign_transport => [ 172 | "eth_blockNumber" => 173 | req => json!([]), 174 | res => json!("0x1011"); 175 | "eth_getLogs" => 176 | req => json!([{ 177 | "address": ["0x00000000000000000000000000000000000000ee"], 178 | "fromBlock": "0x1", 179 | "limit": null, 180 | "toBlock": "0x1005", 181 | "topics": [[COLLECTED_SIGNATURES_TOPIC], null, null, null] 182 | }]), 183 | res => json!([{ 184 | "address": "0x00000000000000000000000000000000000000ee", 185 | "topics": [COLLECTED_SIGNATURES_TOPIC], 186 | "data": "0x000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebcccc00000000000000000000000000000000000000000000000000000000000000f0", 187 | "type": "", 188 | "transactionHash": "0x884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a9424364" 189 | }]); 190 | // call to `message` 191 | "eth_call" => 192 | req => json!([{ 193 | "data": "0x490a32c600000000000000000000000000000000000000000000000000000000000000f0", 194 | "to": "0x00000000000000000000000000000000000000ee" 195 | }, "latest"]), 196 | res => json!(format!("0x{}", MessageToMainnet { 197 | recipient: [1u8; 20].into(), 198 | value: 10000.into(), 199 | sidenet_transaction_hash: "0x884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a9424364".into(), 200 | mainnet_gas_price: 1000.into(), 201 | }.to_payload().to_hex())); 202 | // calls to `signature` 203 | "eth_call" => 204 | req => json!([{ 205 | "data": "0x1812d99600000000000000000000000000000000000000000000000000000000000000f00000000000000000000000000000000000000000000000000000000000000000", 206 | "to": "0x00000000000000000000000000000000000000ee" 207 | },"latest"]), 208 | res => json!(format!("0x{}", Signature { v: 1, r: 2.into(), s: 3.into() }.to_payload().to_hex())); 209 | "eth_call" => 210 | req => json!([{ 211 | "data": "0x1812d99600000000000000000000000000000000000000000000000000000000000000f00000000000000000000000000000000000000000000000000000000000000001", 212 | "to": "0x00000000000000000000000000000000000000ee" 213 | },"latest"]), 214 | res => json!(format!("0x{}", Signature { v: 4, r: 5.into(), s: 6.into() }.to_payload().to_hex())); 215 | ] 216 | } 217 | -------------------------------------------------------------------------------- /tools/estimate_gas_costs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # prints out estimated gas costs of contract functions. 4 | # runs the tests which estimate and print out gas costs. then greps test output for gas costs. 5 | 6 | cd truffle 7 | yarn test | grep "estimated gas cost" 8 | -------------------------------------------------------------------------------- /tools/solc_compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd contracts 4 | solc --abi --bin -o . --overwrite bridge.sol 5 | 6 | for abi in *.abi; do 7 | python -m json.tool "$abi" > tmp 8 | cat tmp > "$abi" 9 | done 10 | 11 | rm tmp 12 | -------------------------------------------------------------------------------- /truffle/.node-xmlhttprequest-content-37792: -------------------------------------------------------------------------------- 1 | {"err":null,"data":{"statusCode":200,"headers":{"access-control-allow-headers":"Origin, X-Requested-With, Content-Type, Accept","access-control-allow-origin":"*","access-control-allow-methods":"*","content-type":"application/json","date":"Fri, 26 Jan 2018 09:06:17 GMT","connection":"close","transfer-encoding":"chunked"},"text":"{\"id\":723,\"jsonrpc\":\"2.0\",\"result\":\"0x00000000000000056bc75e2d63100000\"}"}} -------------------------------------------------------------------------------- /truffle/.node-xmlhttprequest-sync-37792: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omni/poa-bridge/924e0ad79c216eac139b600d466d39c9f530dcda/truffle/.node-xmlhttprequest-sync-37792 -------------------------------------------------------------------------------- /truffle/.soliumignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omni/poa-bridge/924e0ad79c216eac139b600d466d39c9f530dcda/truffle/.soliumignore -------------------------------------------------------------------------------- /truffle/.soliumrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solium:all", 3 | "plugins": [ 4 | "security" 5 | ], 6 | "rules": { 7 | "quotes": [ 8 | "error", 9 | "double" 10 | ], 11 | "indentation": [ 12 | "error", 13 | 4 14 | ], 15 | "arg-overflow": [ 16 | "warning", 17 | 4 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /truffle/contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.4; 2 | 3 | 4 | contract Migrations { 5 | address public owner; 6 | uint public last_completed_migration; 7 | 8 | modifier restricted() { 9 | if (msg.sender == owner) { 10 | _; 11 | } 12 | } 13 | 14 | function Migrations() public { 15 | owner = msg.sender; 16 | } 17 | 18 | function setCompleted(uint completed) public restricted { 19 | last_completed_migration = completed; 20 | } 21 | 22 | function upgrade(address newAddress) public restricted { 23 | Migrations upgraded = Migrations(newAddress); 24 | upgraded.setCompleted(last_completed_migration); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /truffle/contracts/bridge.sol: -------------------------------------------------------------------------------- 1 | ../../contracts/bridge.sol -------------------------------------------------------------------------------- /truffle/migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /truffle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parity-bridge", 3 | "version": "1.0.0", 4 | "description": "Bridge between any two ethereum-based networks", 5 | "license": "GPL-3.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/paritytech/parity-bridge.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/paritytech/parity-bridge/issues" 12 | }, 13 | "homepage": "https://github.com/paritytech/parity-bridge", 14 | "devDependencies": { 15 | "concurrently": "^3.5.1", 16 | "coveralls": "^3.0.0", 17 | "ganache-cli": "^6.0.3", 18 | "solidity-coverage": "^0.4.8", 19 | "solium": "^1.1.2", 20 | "truffle": "^4.0.4" 21 | }, 22 | "dependencies": {}, 23 | "scripts": { 24 | "ci": "concurrently \"yarn run solium\" \"yarn run truffle-with-rpc\" \"yarn run solidity-coverage\"", 25 | "ganache": "ganache-cli --port 8547", 26 | "solidity-coverage": "solidity-coverage", 27 | "solium": "solium --dir contracts/", 28 | "test": "yarn run truffle-with-rpc", 29 | "truffle": "truffle test", 30 | "truffle-with-rpc": "concurrently --success first --kill-others \"yarn run ganache\" \"yarn run truffle\"" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /truffle/test/foreign-erc20.js: -------------------------------------------------------------------------------- 1 | var ForeignBridge = artifacts.require("ForeignBridge"); 2 | var helpers = require("./helpers/helpers"); 3 | 4 | contract('ForeignBridge', function(accounts) { 5 | it("totalSupply", function() { 6 | var contract; 7 | var requiredSignatures = 1; 8 | var estimatedGasCostOfWithdraw = 0; 9 | var authorities = [accounts[0], accounts[1]]; 10 | var owner = accounts[2]; 11 | var hash = "0xe55bb43c36cdf79e23b4adc149cdded921f0d482e613c50c6540977c213bc408"; 12 | var value = web3.toWei(3, "ether"); 13 | 14 | return ForeignBridge.new(requiredSignatures, authorities, estimatedGasCostOfWithdraw).then(function(instance) { 15 | contract = instance; 16 | 17 | return contract.totalSupply(); 18 | }).then(function(result) { 19 | assert.equal(0, result, "initial supply should be 0"); 20 | 21 | return contract.deposit(owner, value, hash, {from: authorities[0]}); 22 | }).then(function(result) { 23 | 24 | return contract.totalSupply(); 25 | }).then(function(result) { 26 | console.log(result); 27 | assert(result.equals(value), "deposit should increase supply"); 28 | 29 | var homeGasPrice = 1000; 30 | return contract.transferHomeViaRelay(owner, value, homeGasPrice, {from: owner}); 31 | }).then(function() { 32 | 33 | return contract.totalSupply(); 34 | }).then(function(result) { 35 | assert.equal(0, result, "home transfer should decrease supply"); 36 | }) 37 | }) 38 | 39 | it("should be able to approve others to spend tokens in their name", function() { 40 | var contract; 41 | var requiredSignatures = 1; 42 | var estimatedGasCostOfWithdraw = 0; 43 | var authorities = [accounts[0], accounts[1]]; 44 | var owner = accounts[2]; 45 | var spender = accounts[3]; 46 | var receiver = accounts[4]; 47 | var hash = "0xe55bb43c36cdf79e23b4adc149cdded921f0d482e613c50c6540977c213bc408"; 48 | 49 | return ForeignBridge.new(requiredSignatures, authorities, estimatedGasCostOfWithdraw).then(function(instance) { 50 | contract = instance; 51 | 52 | // deposit something so we can transfer it 53 | return contract.deposit(owner, web3.toWei(3, "ether"), hash, {from: authorities[0]}); 54 | }).then(function(result) { 55 | 56 | return contract.allowance(owner, spender); 57 | }).then(function(result) { 58 | assert.equal(0, result, "initial allowance should be 0"); 59 | 60 | return contract.transferFrom(owner, receiver, web3.toWei(1, "ether"), {from: spender}) 61 | .then(function() { 62 | assert(false, "transfer without allowance should fail"); 63 | }, helpers.ignoreExpectedError) 64 | }).then(function() { 65 | 66 | // transfer 0 without allowance should work 67 | return contract.transferFrom(owner, receiver, 0, {from: spender}); 68 | }).then(function(result) { 69 | assert.equal(1, result.logs.length, "Exactly one event should be created"); 70 | assert.equal("Transfer", result.logs[0].event, "Event name should be Transfer"); 71 | assert.equal(owner, result.logs[0].args.from); 72 | assert.equal(receiver, result.logs[0].args.to); 73 | assert.equal(0, result.logs[0].args.tokens); 74 | 75 | // transfer should work 76 | return contract.approve(spender, web3.toWei(4, "ether"), {from: owner}); 77 | }).then(function(result) { 78 | assert.equal(1, result.logs.length, "Exactly one event should be created"); 79 | assert.equal("Approval", result.logs[0].event, "Event name should be Approval"); 80 | assert.equal(owner, result.logs[0].args.tokenOwner); 81 | assert.equal(spender, result.logs[0].args.spender); 82 | assert.equal(web3.toWei(4, "ether"), result.logs[0].args.tokens); 83 | 84 | return contract.allowance(owner, spender); 85 | }).then(function(result) { 86 | assert.equal(web3.toWei(4, "ether"), result, "approval should set allowance"); 87 | 88 | return contract.transferFrom(owner, receiver, web3.toWei(4, "ether"), {from: spender}) 89 | .then(function() { 90 | assert(false, "transferring more than balance should fail"); 91 | }, helpers.ignoreExpectedError) 92 | }).then(function() { 93 | 94 | return contract.approve(spender, web3.toWei(2, "ether"), {from: owner}); 95 | }).then(function(result) { 96 | assert.equal(1, result.logs.length, "Exactly one event should be created"); 97 | assert.equal("Approval", result.logs[0].event, "Event name should be Approval"); 98 | assert.equal(owner, result.logs[0].args.tokenOwner); 99 | assert.equal(spender, result.logs[0].args.spender); 100 | assert.equal(web3.toWei(2, "ether"), result.logs[0].args.tokens); 101 | 102 | return contract.allowance(owner, spender); 103 | }).then(function(result) { 104 | assert.equal(web3.toWei(2, "ether"), result, "approval should update allowance"); 105 | 106 | return contract.transferFrom(owner, receiver, web3.toWei(2, "ether") + 2, {from: spender}) 107 | .then(function() { 108 | assert(false, "transferring more than allowance should fail"); 109 | }, helpers.ignoreExpectedError) 110 | }).then(function() { 111 | 112 | return contract.transferFrom(owner, receiver, web3.toWei(2, "ether"), {from: spender}); 113 | }).then(function(result) { 114 | assert.equal(1, result.logs.length, "Exactly one event should be created"); 115 | assert.equal("Transfer", result.logs[0].event, "Event name should be Transfer"); 116 | assert.equal(owner, result.logs[0].args.from); 117 | assert.equal(receiver, result.logs[0].args.to); 118 | assert.equal(web3.toWei(2, "ether"), result.logs[0].args.tokens); 119 | 120 | return contract.balanceOf(owner); 121 | }).then(function(result) { 122 | assert.equal(web3.toWei(1, "ether"), result, "transferring should reduce owners balance"); 123 | 124 | return contract.balanceOf(receiver); 125 | }).then(function(result) { 126 | assert.equal(web3.toWei(2, "ether"), result, "transferring should increase receivers balance"); 127 | 128 | return contract.balanceOf(spender); 129 | }).then(function(result) { 130 | assert.equal(0, result, "transferring should not modify spenders balance"); 131 | 132 | return contract.allowance(owner, spender); 133 | }).then(function(result) { 134 | assert.equal(0, result, "transferring whole allowance should set allowance to 0"); 135 | }) 136 | }) 137 | 138 | it("should allow user to transfer value locally", function() { 139 | var meta; 140 | var requiredSignatures = 1; 141 | var estimatedGasCostOfWithdraw = 0; 142 | var authorities = [accounts[0], accounts[1]]; 143 | var userAccount = accounts[2]; 144 | var userAccount2 = accounts[3]; 145 | var user1InitialValue = web3.toWei(3, "ether"); 146 | var transferedValue = web3.toWei(1, "ether"); 147 | var hash = "0xe55bb43c36cdf79e23b4adc149cdded921f0d482e613c50c6540977c213bc408"; 148 | return ForeignBridge.new(requiredSignatures, authorities, estimatedGasCostOfWithdraw).then(function(instance) { 149 | meta = instance; 150 | // top up balance so we can transfer 151 | return meta.deposit(userAccount, user1InitialValue, hash, { from: authorities[0] }); 152 | }).then(function(result) { 153 | return meta.transfer(userAccount2, transferedValue, { from: userAccount }); 154 | }).then(function(result) { 155 | assert.equal(1, result.logs.length, "Exactly one event should be created"); 156 | assert.equal("Transfer", result.logs[0].event, "Event name should be Transfer"); 157 | assert.equal(userAccount, result.logs[0].args.from, "Event from should be transaction sender"); 158 | assert.equal(userAccount2, result.logs[0].args.to, "Event from should be transaction recipient"); 159 | assert.equal(transferedValue, result.logs[0].args.tokens, "Event tokens should match transaction value"); 160 | return Promise.all([ 161 | meta.balances.call(userAccount), 162 | meta.balances.call(userAccount2) 163 | ]) 164 | }).then(function(result) { 165 | assert.equal(web3.toWei(2, "ether"), result[0]); 166 | assert.equal(transferedValue, result[1]); 167 | }) 168 | }) 169 | 170 | it("should not allow user to transfer value they don't have", function() { 171 | var meta; 172 | var requiredSignatures = 1; 173 | var estimatedGasCostOfWithdraw = 0; 174 | var authorities = [accounts[0], accounts[1]]; 175 | var userAccount = accounts[2]; 176 | var recipientAccount = accounts[3]; 177 | var userValue = web3.toWei(3, "ether"); 178 | var transferedValue = web3.toWei(4, "ether"); 179 | var hash = "0xe55bb43c36cdf79e23b4adc149cdded921f0d482e613c50c6540977c213bc408"; 180 | return ForeignBridge.new(requiredSignatures, authorities, estimatedGasCostOfWithdraw).then(function(instance) { 181 | meta = instance; 182 | return meta.deposit(userAccount, userValue, hash, { from: authorities[0] }); 183 | }).then(function(result) { 184 | return meta.transfer(recipientAccount, transferedValue, { from: userAccount }) 185 | .then(function() { 186 | assert(false, "transfer should fail"); 187 | }, helpers.ignoreExpectedError) 188 | }) 189 | }) 190 | 191 | it("should allow transfer of 0 value according to ERC20", function() { 192 | var meta; 193 | var requiredSignatures = 1; 194 | var estimatedGasCostOfWithdraw = 0; 195 | var authorities = [accounts[0], accounts[1]]; 196 | var userAccount = accounts[2]; 197 | var recipientAccount = accounts[3]; 198 | var userValue = web3.toWei(3, "ether"); 199 | var hash = "0xe55bb43c36cdf79e23b4adc149cdded921f0d482e613c50c6540977c213bc408"; 200 | return ForeignBridge.new(requiredSignatures, authorities, estimatedGasCostOfWithdraw).then(function(instance) { 201 | meta = instance; 202 | return meta.deposit(userAccount, userValue, hash, { from: authorities[0] }); 203 | }).then(function(result) { 204 | return meta.transfer(recipientAccount, 0, { from: userAccount }); 205 | }).then(function(result) { 206 | assert.equal(1, result.logs.length, "Exactly one event should be created"); 207 | assert.equal("Transfer", result.logs[0].event, "Event name should be Transfer"); 208 | assert.equal(userAccount, result.logs[0].args.from, "Event from should be transaction sender"); 209 | assert.equal(recipientAccount, result.logs[0].args.to, "Event from should be transaction recipient"); 210 | assert.equal(0, result.logs[0].args.tokens, "Event tokens should match transaction value"); 211 | return Promise.all([ 212 | meta.balances.call(userAccount), 213 | meta.balances.call(recipientAccount) 214 | ]) 215 | }).then(function(result) { 216 | assert.equal(userValue, result[0]); 217 | assert.equal(0, result[1]); 218 | }) 219 | }) 220 | 221 | it("transfer that results in overflow should fail", function() { 222 | var meta; 223 | var requiredSignatures = 1; 224 | var estimatedGasCostOfWithdraw = 0; 225 | var authorities = [accounts[0], accounts[1]]; 226 | var userAccount = accounts[2]; 227 | var recipientAccount = accounts[3]; 228 | var maxValue = web3.toWei("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "wei"); 229 | var hash = "0xe55bb43c36cdf79e23b4adc149cdded921f0d482e613c50c6540977c213bc408"; 230 | return ForeignBridge.new(requiredSignatures, authorities, estimatedGasCostOfWithdraw).then(function(instance) { 231 | meta = instance; 232 | return meta.deposit(recipientAccount, maxValue, hash, { from: authorities[0] }); 233 | }).then(function(result) { 234 | return meta.deposit(userAccount, 1, hash, { from: authorities[0] }); 235 | }).then(function(result) { 236 | return meta.transfer(recipientAccount, 1, { from: userAccount }) 237 | .then(function() { 238 | assert(false, "transfer should fail"); 239 | }, helpers.ignoreExpectedError) 240 | }) 241 | }) 242 | 243 | it("transferFrom that results in overflow should fail", function() { 244 | var meta; 245 | var requiredSignatures = 1; 246 | var estimatedGasCostOfWithdraw = 0; 247 | var authorities = [accounts[0], accounts[1]]; 248 | var userAccount = accounts[2]; 249 | var spenderAccount = accounts[3]; 250 | var recipientAccount = accounts[4]; 251 | var maxValue = web3.toWei("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "wei"); 252 | var hash = "0xe55bb43c36cdf79e23b4adc149cdded921f0d482e613c50c6540977c213bc408"; 253 | return ForeignBridge.new(requiredSignatures, authorities, estimatedGasCostOfWithdraw).then(function(instance) { 254 | meta = instance; 255 | return meta.deposit(recipientAccount, maxValue, hash, { from: authorities[0] }); 256 | }).then(function(result) { 257 | return meta.deposit(userAccount, 1, hash, { from: authorities[0] }); 258 | }).then(function(result) { 259 | return meta.approve(spenderAccount, 1, {from: userAccount}); 260 | }).then(function(result) { 261 | return meta.transferFrom(userAccount, recipientAccount, 1, { from: spenderAccount }) 262 | .then(function() { 263 | assert(false, "transfer should fail"); 264 | }, helpers.ignoreExpectedError) 265 | }) 266 | }) 267 | }) 268 | -------------------------------------------------------------------------------- /truffle/test/helpers.js: -------------------------------------------------------------------------------- 1 | // solidity Helpers library 2 | var Helpers = artifacts.require("HelpersTest"); 3 | // testing helpers 4 | var helpers = require("./helpers/helpers"); 5 | 6 | contract("Helpers", function(accounts) { 7 | it("`addressArrayContains` should function correctly", function() { 8 | var addresses = accounts.slice(0, 3); 9 | var otherAddress = accounts[3]; 10 | var library; 11 | return Helpers.new().then(function(instance) { 12 | library = instance; 13 | 14 | return library.addressArrayContains.call([], otherAddress); 15 | }).then(function(result) { 16 | assert.equal(result, false, "should return false for empty array"); 17 | 18 | return library.addressArrayContains.call([otherAddress], otherAddress); 19 | }).then(function(result) { 20 | assert.equal(result, true, "should return true for singleton array containing value"); 21 | 22 | return library.addressArrayContains.call([addresses[0]], addresses[1]); 23 | }).then(function(result) { 24 | assert.equal(result, false, "should return false for singleton array not containing value"); 25 | 26 | return library.addressArrayContains.call(addresses, addresses[0]); 27 | }).then(function(result) { 28 | assert.equal(result, true); 29 | 30 | return library.addressArrayContains.call(addresses, addresses[1]); 31 | }).then(function(result) { 32 | assert.equal(result, true); 33 | 34 | return library.addressArrayContains.call(addresses, addresses[2]); 35 | }).then(function(result) { 36 | assert.equal(result, true); 37 | 38 | return library.addressArrayContains.call(addresses, otherAddress); 39 | }).then(function(result) { 40 | assert.equal(result, false); 41 | }) 42 | }) 43 | 44 | it("`uintToString` should convert int to string", function() { 45 | var numbersFrom1To100 = helpers.range(1, 101); 46 | var library; 47 | return Helpers.new().then(function(instance) { 48 | library = instance; 49 | 50 | return library.uintToString.call(0) 51 | }).then(function(result) { 52 | assert.equal(result, "0"); 53 | 54 | return Promise.all(numbersFrom1To100.map(function(number) { 55 | return library.uintToString.call(number); 56 | })); 57 | }).then(function(result) { 58 | assert.deepEqual(result, numbersFrom1To100.map(function(number) { 59 | return number.toString(); 60 | }), "should convert numbers from 1 to 100 correctly"); 61 | 62 | return library.uintToString.estimateGas(1); 63 | }).then(function(result) { 64 | console.log("estimated gas cost of Helpers.uintToString(1)", result); 65 | 66 | return library.uintToString.call(1234) 67 | }).then(function(result) { 68 | assert.equal(result, "1234"); 69 | 70 | return library.uintToString.call(12345678) 71 | }).then(function(result) { 72 | assert.equal(result, "12345678"); 73 | 74 | return library.uintToString.estimateGas(12345678) 75 | }).then(function(result) { 76 | console.log("estimated gas cost of Helpers.uintToString(12345678)", result); 77 | 78 | return library.uintToString.call(web3.toBigNumber("131242344353464564564574574567456")); 79 | }).then(function(result) { 80 | assert.equal(result, "131242344353464564564574574567456"); 81 | }) 82 | }) 83 | 84 | it("`hasEnoughValidSignatures` should pass for 1 required signature", function() { 85 | var library; 86 | var signature; 87 | var requiredSignatures = 1; 88 | var authorities = [accounts[0], accounts[1]]; 89 | var recipientAccount = accounts[2]; 90 | var value = web3.toBigNumber(web3.toWei(1, "ether")); 91 | var homeGasPrice = web3.toBigNumber(10000); 92 | var message = helpers.createMessage(recipientAccount, value, "0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80", homeGasPrice); 93 | 94 | return Helpers.new().then(function(instance) { 95 | library = instance; 96 | }).then(function(result) { 97 | return helpers.sign(authorities[0], message); 98 | }).then(function(result) { 99 | signature = result; 100 | var vrs = helpers.signatureToVRS(signature); 101 | 102 | return library.hasEnoughValidSignatures.call( 103 | message, 104 | [vrs.v], 105 | [vrs.r], 106 | [vrs.s], 107 | authorities, 108 | requiredSignatures 109 | ).then(function(result) { 110 | assert(result, "should return true"); 111 | }) 112 | }) 113 | }) 114 | 115 | it("`verifySignatures` should pass for multiple signatures", function() { 116 | var library; 117 | var signatures = []; 118 | var requiredSignatures = 3; 119 | var authorities = [accounts[0], accounts[1], accounts[2]]; 120 | var recipientAccount = accounts[3]; 121 | var value = web3.toBigNumber(web3.toWei(1, "ether")); 122 | var homeGasPrice = web3.toBigNumber(10000); 123 | var message = helpers.createMessage(recipientAccount, value, "0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80", homeGasPrice); 124 | 125 | return Helpers.new().then(function(instance) { 126 | library = instance; 127 | }).then(function(result) { 128 | 129 | return helpers.sign(authorities[0], message); 130 | }).then(function(result) { 131 | signatures[0] = result; 132 | 133 | return helpers.sign(authorities[1], message); 134 | }).then(function(result) { 135 | signatures[1] = result; 136 | 137 | return helpers.sign(authorities[2], message); 138 | }).then(function(result) { 139 | signatures[2] = result; 140 | 141 | var vrs = []; 142 | vrs[0] = helpers.signatureToVRS(signatures[0]); 143 | vrs[1] = helpers.signatureToVRS(signatures[1]); 144 | vrs[2] = helpers.signatureToVRS(signatures[2]); 145 | 146 | return library.hasEnoughValidSignatures.call( 147 | message, 148 | [vrs[0].v, vrs[1].v, vrs[2].v], 149 | [vrs[0].r, vrs[1].r, vrs[2].r], 150 | [vrs[0].s, vrs[1].s, vrs[2].s], 151 | authorities, 152 | requiredSignatures 153 | ).then(function(result) { 154 | assert(result, "should return true"); 155 | }) 156 | }) 157 | }) 158 | 159 | it("`verifySignatures` should fail for signature for other message", function() { 160 | var library; 161 | var signature; 162 | var requiredSignatures = 1; 163 | var authorities = [accounts[0], accounts[1]]; 164 | var recipientAccount = accounts[2]; 165 | var value = web3.toBigNumber(web3.toWei(1, "ether")); 166 | var homeGasPrice = web3.toBigNumber(10000); 167 | var homeGasPrice2 = web3.toBigNumber(100); 168 | var message = helpers.createMessage(recipientAccount, value, "0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80", homeGasPrice); 169 | var message2 = helpers.createMessage(recipientAccount, value, "0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80", homeGasPrice2); 170 | 171 | return Helpers.new().then(function(instance) { 172 | library = instance; 173 | }).then(function(result) { 174 | return helpers.sign(authorities[0], message); 175 | }).then(function(result) { 176 | signature = result; 177 | var vrs = helpers.signatureToVRS(signature); 178 | 179 | return library.hasEnoughValidSignatures.call( 180 | message2, 181 | [vrs.v], 182 | [vrs.r], 183 | [vrs.s], 184 | authorities, 185 | requiredSignatures 186 | ).then(function(result) { 187 | assert.equal(result, false, "should return false"); 188 | }) 189 | }) 190 | }) 191 | 192 | it("`verifySignatures` should fail if signer not in addresses", function() { 193 | var library; 194 | var signature; 195 | var requiredSignatures = 1; 196 | var authorities = [accounts[0], accounts[1]]; 197 | var recipientAccount = accounts[2]; 198 | var value = web3.toBigNumber(web3.toWei(1, "ether")); 199 | var homeGasPrice = web3.toBigNumber(10000); 200 | var message = helpers.createMessage(recipientAccount, value, "0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80", homeGasPrice); 201 | 202 | return Helpers.new().then(function(instance) { 203 | library = instance; 204 | }).then(function(result) { 205 | return helpers.sign(accounts[3], message); 206 | }).then(function(result) { 207 | signature = result; 208 | var vrs = helpers.signatureToVRS(signature); 209 | 210 | return library.hasEnoughValidSignatures.call( 211 | message, 212 | [vrs.v], 213 | [vrs.r], 214 | [vrs.s], 215 | authorities, 216 | requiredSignatures 217 | ).then(function(result) { 218 | assert.equal(result, false, "should return false"); 219 | }) 220 | }) 221 | }) 222 | 223 | it("`verifySignatures` should fail for not enough signatures", function() { 224 | var library; 225 | var signature; 226 | var requiredSignatures = 2; 227 | var authorities = [accounts[0], accounts[1]]; 228 | var recipientAccount = accounts[2]; 229 | var value = web3.toBigNumber(web3.toWei(1, "ether")); 230 | var homeGasPrice = web3.toBigNumber(10000); 231 | var message = helpers.createMessage(recipientAccount, value, "0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80", homeGasPrice); 232 | 233 | return Helpers.new().then(function(instance) { 234 | library = instance; 235 | }).then(function(result) { 236 | return helpers.sign(authorities[0], message); 237 | }).then(function(result) { 238 | signature = result; 239 | var vrs = helpers.signatureToVRS(signature); 240 | 241 | return library.hasEnoughValidSignatures.call( 242 | message, 243 | [vrs.v], 244 | [vrs.r], 245 | [vrs.s], 246 | authorities, 247 | requiredSignatures 248 | ).then(function(result) { 249 | assert.equal(result, false, "should return false"); 250 | }) 251 | }) 252 | }) 253 | 254 | it("`verifySignatures` should fail for duplicated signature", function() { 255 | var library; 256 | var signature; 257 | var requiredSignatures = 2; 258 | var authorities = [accounts[0], accounts[1]]; 259 | var recipientAccount = accounts[2]; 260 | var value = web3.toBigNumber(web3.toWei(1, "ether")); 261 | var homeGasPrice = web3.toBigNumber(10000); 262 | var message = helpers.createMessage(recipientAccount, value, "0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80", homeGasPrice); 263 | 264 | return Helpers.new().then(function(instance) { 265 | library = instance; 266 | }).then(function(result) { 267 | return helpers.sign(authorities[0], message); 268 | }).then(function(result) { 269 | signature = result; 270 | var vrs = helpers.signatureToVRS(signature); 271 | 272 | return library.hasEnoughValidSignatures.call( 273 | message, 274 | [vrs.v, vrs.v], 275 | [vrs.r, vrs.r], 276 | [vrs.s, vrs.r], 277 | authorities, 278 | requiredSignatures 279 | ).then(function(result) { 280 | assert.equal(result, false, "should return false"); 281 | }) 282 | }) 283 | }) 284 | }) 285 | -------------------------------------------------------------------------------- /truffle/test/helpers/helpers.js: -------------------------------------------------------------------------------- 1 | // returns a Promise that resolves with a hex string that is the signature of 2 | // `data` signed with the key of `address` 3 | function sign(address, data) { 4 | return new Promise(function(resolve, reject) { 5 | web3.eth.sign(address, data, function(err, result) { 6 | if (err !== null) { 7 | return reject(err); 8 | } else { 9 | return resolve(normalizeSignature(result)); 10 | //return resolve(result); 11 | } 12 | }) 13 | }) 14 | } 15 | module.exports.sign = sign; 16 | 17 | // geth && testrpc has different output of eth_sign than parity 18 | // https://github.com/ethereumjs/testrpc/issues/243#issuecomment-326750236 19 | function normalizeSignature(signature) { 20 | signature = strip0x(signature); 21 | 22 | // increase v by 27... 23 | return "0x" + signature.substr(0, 128) + (parseInt(signature.substr(128), 16) + 27).toString(16); 24 | } 25 | module.exports.normalizeSignature = normalizeSignature; 26 | 27 | // strips leading "0x" if present 28 | function strip0x(input) { 29 | return input.replace(/^0x/, ""); 30 | } 31 | module.exports.strip0x = strip0x; 32 | 33 | // extracts and returns the `v`, `r` and `s` values from a `signature`. 34 | // all inputs and outputs are hex strings with leading '0x'. 35 | function signatureToVRS(signature) { 36 | assert.equal(signature.length, 2 + 32 * 2 + 32 * 2 + 2); 37 | signature = strip0x(signature); 38 | var v = parseInt(signature.substr(64 * 2), 16); 39 | var r = "0x" + signature.substr(0, 32 * 2); 40 | var s = "0x" + signature.substr(32 * 2, 32 * 2); 41 | return {v: v, r: r, s: s}; 42 | } 43 | module.exports.signatureToVRS = signatureToVRS; 44 | 45 | // returns BigNumber `num` converted to a little endian hex string 46 | // that is exactly 32 bytes long. 47 | // `num` must represent an unsigned integer 48 | function bigNumberToPaddedBytes32(num) { 49 | assert(web3._extend.utils.isBigNumber(num)); 50 | assert(num.isInteger()); 51 | assert(!num.isNegative()); 52 | var result = strip0x(num.toString(16)); 53 | while (result.length < 64) { 54 | result = "0" + result; 55 | } 56 | return "0x" + result; 57 | } 58 | module.exports.bigNumberToPaddedBytes32 = bigNumberToPaddedBytes32; 59 | 60 | // returns an promise that resolves to an object 61 | // that maps `addresses` to their current balances 62 | function getBalances(addresses) { 63 | return Promise.all(addresses.map(function(address) { 64 | return web3.eth.getBalance(address); 65 | })).then(function(balancesArray) { 66 | let addressToBalance = {}; 67 | addresses.forEach(function(address, index) { 68 | addressToBalance[address] = balancesArray[index]; 69 | }); 70 | return addressToBalance; 71 | }) 72 | } 73 | module.exports.getBalances = getBalances; 74 | 75 | 76 | // returns hex string of the bytes of the message 77 | // composed from `recipient`, `value` and `transactionHash` 78 | // that is relayed from `foreign` to `home` on withdraw 79 | function createMessage(recipient, value, transactionHash, homeGasPrice) { 80 | web3._extend.utils.isBigNumber(value); 81 | recipient = strip0x(recipient); 82 | assert.equal(recipient.length, 20 * 2); 83 | 84 | var value = strip0x(bigNumberToPaddedBytes32(value)); 85 | assert.equal(value.length, 64); 86 | 87 | transactionHash = strip0x(transactionHash); 88 | assert.equal(transactionHash.length, 32 * 2); 89 | 90 | web3._extend.utils.isBigNumber(homeGasPrice); 91 | homeGasPrice = strip0x(bigNumberToPaddedBytes32(homeGasPrice)); 92 | assert.equal(homeGasPrice.length, 64); 93 | 94 | var message = "0x" + recipient + value + transactionHash + homeGasPrice; 95 | var expectedMessageLength = (20 + 32 + 32 + 32) * 2 + 2; 96 | assert.equal(message.length, expectedMessageLength); 97 | return message; 98 | } 99 | module.exports.createMessage = createMessage; 100 | 101 | // returns array of integers progressing from `start` up to, but not including, `end` 102 | function range(start, end) { 103 | var result = []; 104 | for (var i = start; i < end; i++) { 105 | result.push(i); 106 | } 107 | return result; 108 | } 109 | module.exports.range = range; 110 | 111 | // just used to signal/document that we're explicitely ignoring/expecting an error 112 | function ignoreExpectedError() { 113 | } 114 | module.exports.ignoreExpectedError = ignoreExpectedError; 115 | -------------------------------------------------------------------------------- /truffle/test/message.js: -------------------------------------------------------------------------------- 1 | var Message = artifacts.require("MessageTest"); 2 | var helpers = require("./helpers/helpers"); 3 | 4 | contract("Message", function(accounts) { 5 | var recipientAccount = accounts[0]; 6 | var value = web3.toBigNumber(web3.toWei(1, "ether")); 7 | var transactionHash = "0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80"; 8 | var homeGasPrice = web3.toBigNumber(web3.toWei(3, "gwei")); 9 | var message = helpers.createMessage(recipientAccount, value, transactionHash, homeGasPrice); 10 | 11 | it("should extract value", function() { 12 | return Message.new().then(function(instance) { 13 | return instance.getValue.call(message) 14 | }).then(function(result) { 15 | assert(result.equals(value)); 16 | }) 17 | }) 18 | 19 | it("should extract recipient", function() { 20 | return Message.new().then(function(instance) { 21 | return instance.getRecipient.call(message) 22 | }).then(function(result) { 23 | assert.equal(result, recipientAccount); 24 | }) 25 | }) 26 | 27 | it("should extract transactionHash", function() { 28 | return Message.new().then(function(instance) { 29 | return instance.getTransactionHash.call(message) 30 | }).then(function(result) { 31 | assert.equal(result, transactionHash); 32 | }) 33 | }) 34 | 35 | it("should extract homeGasPrice", function() { 36 | return Message.new().then(function(instance) { 37 | return instance.getHomeGasPrice.call(message) 38 | }).then(function(result) { 39 | assert(result.equals(homeGasPrice)); 40 | }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /truffle/test/message_signing.js: -------------------------------------------------------------------------------- 1 | var MessageSigning = artifacts.require("MessageSigningTest"); 2 | var helpers = require("./helpers/helpers"); 3 | 4 | contract("MessageSigning", function(accounts) { 5 | it("should recover address from signed message", function() { 6 | var signature = "0xb585c41f3cceb2ff9b5c033f2edbefe93415bde365489c989bad8cef3b18e38148a13e100608a29735d709fe708926d37adcecfffb32b1d598727028a16df5db1b"; 7 | var message = "0xdeadbeaf"; 8 | var account = "0x006e27b6a72e1f34c626762f3c4761547aff1421"; 9 | 10 | return MessageSigning.new().then(function(instance) { 11 | return instance.recoverAddressFromSignedMessage.call(signature, message) 12 | }).then(function(result) { 13 | assert.equal(account, result); 14 | }) 15 | }) 16 | 17 | it("should recover address from long signed message", function() { 18 | var signature = "0x3c9158597e22fa43fcc6636399c560441808e1d8496de0108e401a2ad71022b15d1191cf3c96e06759601c8e00ce7f03f350c12b19d0a8ba3ab3c07a71063f2b1c"; 19 | var message = "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"; 20 | var account = "0x006e27b6a72e1f34c626762f3c4761547aff1421"; 21 | 22 | return MessageSigning.new().then(function(instance) { 23 | return instance.recoverAddressFromSignedMessage.call(signature, message) 24 | }).then(function(result) { 25 | assert.equal(account, result); 26 | }) 27 | }) 28 | 29 | it("should fail to recover address from signature that is too short", function() { 30 | var signature = "0x3c9158597e22fa43fcc6636399c560441808e1d8496de0108e401a2ad71022b15d1191cf3c96e06759601c8e00ce7f03f350c12b19d0a8ba3ab3c07a71063f2b"; 31 | var message = "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"; 32 | var account = "0x006e27b6a72e1f34c626762f3c4761547aff1421"; 33 | 34 | return MessageSigning.new().then(function(instance) { 35 | return instance.recoverAddressFromSignedMessage.call(signature, message) 36 | .then(function() { 37 | assert(false, "should fail because signature is too short"); 38 | }, helpers.ignoreExpectedError) 39 | }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /truffle/truffle.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | networks: { 3 | development: { 4 | host: "localhost", 5 | port: 8547, 6 | network_id: "*", // Match any network id 7 | } 8 | } 9 | }; 10 | --------------------------------------------------------------------------------