diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3cf252a24..85f2de478 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -38,7 +38,7 @@ linux-stable: - aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/x86_64-unknown-linux-gnu/parity.md5 --body parity.md5 - aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/x86_64-unknown-linux-gnu/"parity_"$VER"_amd64.deb" --body "parity_"$VER"_amd64.deb" - aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/x86_64-unknown-linux-gnu/"parity_"$VER"_amd64.deb.md5" --body "parity_"$VER"_amd64.deb.md5" -# - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/x86_64-unknown-linux-gnu + - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/x86_64-unknown-linux-gnu - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1338/push-build/$CI_BUILD_REF_NAME/x86_64-unknown-linux-gnu tags: - rust @@ -107,7 +107,7 @@ linux-centos: - aws s3 rm --recursive s3://$S3_BUCKET/$CI_BUILD_REF_NAME/x86_64-unknown-centos-gnu - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-unknown-centos-gnu/parity --body target/release/parity - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-unknown-centos-gnu/parity.md5 --body parity.md5 -# - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM + - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1338/push-build/$CI_BUILD_REF_NAME/$PLATFORM tags: - rust @@ -146,7 +146,7 @@ linux-i686: - aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/parity.md5 --body parity.md5 - aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/"parity_"$VER"_i386.deb" --body "parity_"$VER"_i386.deb" - aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/"parity_"$VER"_i386.deb.md5" --body "parity_"$VER"_i386.deb.md5" -# - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM + - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1338/push-build/$CI_BUILD_REF_NAME/$PLATFORM tags: - rust @@ -192,7 +192,7 @@ linux-armv7: - aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/parity.md5 --body parity.md5 - aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/"parity_"$VER"_armhf.deb" --body "parity_"$VER"_armhf.deb" - aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/"parity_"$VER"_armhf.deb.md5" --body "parity_"$VER"_armhf.deb.md5" -# - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM + - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1338/push-build/$CI_BUILD_REF_NAME/$PLATFORM tags: - rust @@ -238,7 +238,7 @@ linux-arm: - aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/parity.md5 --body parity.md5 - aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/"parity_"$VER"_armhf.deb" --body "parity_"$VER"_armhf.deb" - aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/"parity_"$VER"_armhf.deb.md5" --body "parity_"$VER"_armhf.deb.md5" -# - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://icarus.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM + - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://icarus.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1338/push-build/$CI_BUILD_REF_NAME/$PLATFORM tags: - rust @@ -277,7 +277,7 @@ linux-armv6: - aws s3 rm --recursive s3://$S3_BUCKET/$CI_BUILD_REF_NAME/$PLATFORM - aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/parity --body target/$PLATFORM/release/parity - aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/parity.md5 --body parity.md5 -# - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM + - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1338/push-build/$CI_BUILD_REF_NAME/$PLATFORM tags: - rust @@ -322,7 +322,7 @@ linux-aarch64: - aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/parity.md5 --body parity.md5 - aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/"parity_"$VER"_arm64.deb" --body "parity_"$VER"_arm64.deb" - aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/"parity_"$VER"_arm64.deb.md5" --body "parity_"$VER"_arm64.deb.md5" -# - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM + - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1338/push-build/$CI_BUILD_REF_NAME/$PLATFORM tags: - rust @@ -359,7 +359,7 @@ darwin: - aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/parity.md5 --body parity.md5 - aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/"parity-"$VER"-osx-installer-EXPERIMENTAL.pkg" --body "parity-"$VER"-osx-installer-EXPERIMENTAL.pkg" - aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/"parity-"$VER"-osx-installer-EXPERIMENTAL.pkg.md5" --body "parity-"$VER"-osx-installer-EXPERIMENTAL.pkg.md5" -# - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM + - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1338/push-build/$CI_BUILD_REF_NAME/$PLATFORM tags: - osx @@ -421,7 +421,7 @@ windows: - aws s3api put-object --bucket %S3_BUCKET% --key %CI_BUILD_REF_NAME%/x86_64-pc-windows-msvc/InstallParity.exe.md5 --body nsis\InstallParity.exe.md5 - aws s3api put-object --bucket %S3_BUCKET% --key %CI_BUILD_REF_NAME%/x86_64-pc-windows-msvc/win-installer.zip --body nsis\win-installer.zip - aws s3api put-object --bucket %S3_BUCKET% --key %CI_BUILD_REF_NAME%/x86_64-pc-windows-msvc/win-installer.zip.md5 --body nsis\win-installer.zip.md5 -# - curl --data "commit=%CI_BUILD_REF%&sha3=%SHA3%&filename=parity.exe&secret=%RELEASES_SECRET%" http://update.parity.io:1337/push-build/%CI_BUILD_REF_NAME%/%PLATFORM% + - curl --data "commit=%CI_BUILD_REF%&sha3=%SHA3%&filename=parity.exe&secret=%RELEASES_SECRET%" http://update.parity.io:1337/push-build/%CI_BUILD_REF_NAME%/%PLATFORM% - curl --data "commit=%CI_BUILD_REF%&sha3=%SHA3%&filename=parity.exe&secret=%RELEASES_SECRET%" http://update.parity.io:1338/push-build/%CI_BUILD_REF_NAME%/%PLATFORM% tags: - rust-windows @@ -535,7 +535,7 @@ push-release: - triggers image: ethcore/rust:stable script: -# - curl --data "secret=$RELEASES_SECRET" http://update.parity.io:1337/push-release/$CI_BUILD_REF_NAME/$CI_BUILD_REF + - curl --data "secret=$RELEASES_SECRET" http://update.parity.io:1337/push-release/$CI_BUILD_REF_NAME/$CI_BUILD_REF - curl --data "secret=$RELEASES_SECRET" http://update.parity.io:1338/push-release/$CI_BUILD_REF_NAME/$CI_BUILD_REF tags: - curl diff --git a/ethcore/src/blockchain/blockchain.rs b/ethcore/src/blockchain/blockchain.rs index 62e9af250..40df0e11a 100644 --- a/ethcore/src/blockchain/blockchain.rs +++ b/ethcore/src/blockchain/blockchain.rs @@ -1329,6 +1329,7 @@ mod tests { use transaction::{Transaction, Action}; use log_entry::{LogEntry, LocalizedLogEntry}; use spec::Spec; + use ethkey::Secret; fn new_db(path: &str) -> Arc { Arc::new(Database::open(&DatabaseConfig::with_columns(::db::NUM_COLUMNS), path).unwrap()) @@ -1467,6 +1468,10 @@ mod tests { // TODO: insert block that already includes one of them as an uncle to check it's not allowed. } + fn secret() -> Secret { + Secret::from_slice(&"".sha3()).unwrap() + } + #[test] fn test_fork_transaction_addresses() { let mut canon_chain = ChainGenerator::default(); @@ -1482,7 +1487,7 @@ mod tests { action: Action::Create, value: 100.into(), data: "601080600c6000396000f3006000355415600957005b60203560003555".from_hex().unwrap(), - }.sign(&"".sha3(), None); + }.sign(&secret(), None); let b1a = canon_chain @@ -1546,7 +1551,7 @@ mod tests { action: Action::Create, value: 100.into(), data: "601080600c6000396000f3006000355415600957005b60203560003555".from_hex().unwrap(), - }.sign(&"".sha3(), None); + }.sign(&secret(), None); let t2 = Transaction { nonce: 1.into(), @@ -1555,7 +1560,7 @@ mod tests { action: Action::Create, value: 100.into(), data: "601080600c6000396000f3006000355415600957005b60203560003555".from_hex().unwrap(), - }.sign(&"".sha3(), None); + }.sign(&secret(), None); let t3 = Transaction { nonce: 2.into(), @@ -1564,7 +1569,7 @@ mod tests { action: Action::Create, value: 100.into(), data: "601080600c6000396000f3006000355415600957005b60203560003555".from_hex().unwrap(), - }.sign(&"".sha3(), None); + }.sign(&secret(), None); let b1a = canon_chain .with_transaction(t1.clone()) @@ -1870,7 +1875,7 @@ mod tests { action: Action::Create, value: 101.into(), data: "601080600c6000396000f3006000355415600957005b60203560003555".from_hex().unwrap(), - }.sign(&"".sha3(), None); + }.sign(&secret(), None); let t2 = Transaction { nonce: 0.into(), gas_price: 0.into(), @@ -1878,7 +1883,7 @@ mod tests { action: Action::Create, value: 102.into(), data: "601080600c6000396000f3006000355415600957005b60203560003555".from_hex().unwrap(), - }.sign(&"".sha3(), None); + }.sign(&secret(), None); let t3 = Transaction { nonce: 0.into(), gas_price: 0.into(), @@ -1886,7 +1891,7 @@ mod tests { action: Action::Create, value: 103.into(), data: "601080600c6000396000f3006000355415600957005b60203560003555".from_hex().unwrap(), - }.sign(&"".sha3(), None); + }.sign(&secret(), None); let tx_hash1 = t1.hash(); let tx_hash2 = t2.hash(); let tx_hash3 = t3.hash(); diff --git a/ethcore/src/client/client.rs b/ethcore/src/client/client.rs index c612bb8fa..befe73ca8 100644 --- a/ethcore/src/client/client.rs +++ b/ethcore/src/client/client.rs @@ -874,6 +874,7 @@ impl BlockChainClient for Client { } fn estimate_gas(&self, t: &SignedTransaction, block: BlockId) -> Result { + const UPPER_CEILING: u64 = 1_000_000_000_000u64; let header = self.block_header(block).ok_or(CallError::StatePruned)?; let last_hashes = self.build_last_hashes(header.parent_hash()); let env_info = EnvInfo { @@ -883,37 +884,38 @@ impl BlockChainClient for Client { difficulty: header.difficulty(), last_hashes: last_hashes, gas_used: U256::zero(), - gas_limit: U256::max_value(), + gas_limit: UPPER_CEILING.into(), }; // that's just a copy of the state. - let mut original_state = self.state_at(block).ok_or(CallError::StatePruned)?; + let original_state = self.state_at(block).ok_or(CallError::StatePruned)?; let sender = t.sender().map_err(|e| { let message = format!("Transaction malformed: {:?}", e); ExecutionError::TransactionMalformed(message) })?; let balance = original_state.balance(&sender); - let needed_balance = t.value + t.gas * t.gas_price; - if balance < needed_balance { - // give the sender a sufficient balance - original_state.add_balance(&sender, &(needed_balance - balance), CleanupMode::NoEmpty); - } let options = TransactOptions { tracing: true, vm_tracing: false, check_nonce: false }; let mut tx = t.clone(); let mut cond = |gas| { - let mut state = original_state.clone(); tx.gas = gas; + + let mut state = original_state.clone(); + let needed_balance = tx.value + tx.gas * tx.gas_price; + if balance < needed_balance { + // give the sender a sufficient balance + state.add_balance(&sender, &(needed_balance - balance), CleanupMode::NoEmpty); + } + Executive::new(&mut state, &env_info, &*self.engine, &self.factories.vm) .transact(&tx, options.clone()) - .map(|r| r.trace[0].result.succeeded()) + .map(|r| r.exception.is_some()) .unwrap_or(false) }; - let mut upper = env_info.gas_limit; + let mut upper = header.gas_limit(); if !cond(upper) { // impossible at block gas limit - try `UPPER_CEILING` instead. // TODO: consider raising limit by powers of two. - const UPPER_CEILING: u64 = 1_000_000_000_000u64; upper = UPPER_CEILING.into(); if !cond(upper) { trace!(target: "estimate_gas", "estimate_gas failed with {}", upper); @@ -1669,7 +1671,7 @@ mod tests { use util::Hashable; // given - let key = KeyPair::from_secret("test".sha3()).unwrap(); + let key = KeyPair::from_secret_slice(&"test".sha3()).unwrap(); let secret = key.secret(); let block_number = 1; diff --git a/ethcore/src/engines/authority_round.rs b/ethcore/src/engines/authority_round.rs index 0986a5aa7..adbc6ba99 100644 --- a/ethcore/src/engines/authority_round.rs +++ b/ethcore/src/engines/authority_round.rs @@ -354,6 +354,7 @@ mod tests { use env_info::EnvInfo; use header::Header; use error::{Error, BlockError}; + use ethkey::Secret; use rlp::encode; use block::*; use tests::helpers::*; @@ -411,8 +412,8 @@ mod tests { #[test] fn generates_seal_and_does_not_double_propose() { let tap = AccountProvider::transient_provider(); - let addr1 = tap.insert_account("1".sha3(), "1").unwrap(); - let addr2 = tap.insert_account("2".sha3(), "2").unwrap(); + let addr1 = tap.insert_account(Secret::from_slice(&"1".sha3()).unwrap(), "1").unwrap(); + let addr2 = tap.insert_account(Secret::from_slice(&"2".sha3()).unwrap(), "2").unwrap(); let spec = Spec::new_test_round(); let engine = &*spec.engine; @@ -445,7 +446,7 @@ mod tests { fn proposer_switching() { let mut header: Header = Header::default(); let tap = AccountProvider::transient_provider(); - let addr = tap.insert_account("0".sha3(), "0").unwrap(); + let addr = tap.insert_account(Secret::from_slice(&"0".sha3()).unwrap(), "0").unwrap(); header.set_author(addr); @@ -464,7 +465,7 @@ mod tests { fn rejects_future_block() { let mut header: Header = Header::default(); let tap = AccountProvider::transient_provider(); - let addr = tap.insert_account("0".sha3(), "0").unwrap(); + let addr = tap.insert_account(Secret::from_slice(&"0".sha3()).unwrap(), "0").unwrap(); header.set_author(addr); diff --git a/ethcore/src/engines/basic_authority.rs b/ethcore/src/engines/basic_authority.rs index 61e25e58f..b3ccfeb43 100644 --- a/ethcore/src/engines/basic_authority.rs +++ b/ethcore/src/engines/basic_authority.rs @@ -201,6 +201,7 @@ mod tests { use error::{BlockError, Error}; use tests::helpers::*; use account_provider::AccountProvider; + use ethkey::Secret; use header::Header; use spec::Spec; use engines::Seal; @@ -261,7 +262,7 @@ mod tests { #[test] fn can_generate_seal() { let tap = AccountProvider::transient_provider(); - let addr = tap.insert_account("".sha3(), "").unwrap(); + let addr = tap.insert_account(Secret::from_slice(&"".sha3()).unwrap(), "").unwrap(); let spec = new_test_authority(); let engine = &*spec.engine; @@ -281,7 +282,7 @@ mod tests { #[test] fn seals_internally() { let tap = AccountProvider::transient_provider(); - let authority = tap.insert_account("".sha3(), "").unwrap(); + let authority = tap.insert_account(Secret::from_slice(&"".sha3()).unwrap(), "").unwrap(); let engine = new_test_authority().engine; assert!(!engine.is_sealer(&Address::default()).unwrap()); diff --git a/ethcore/src/engines/tendermint/message.rs b/ethcore/src/engines/tendermint/message.rs index 02e1276cf..1f17ed902 100644 --- a/ethcore/src/engines/tendermint/message.rs +++ b/ethcore/src/engines/tendermint/message.rs @@ -23,15 +23,35 @@ use header::Header; use rlp::*; use ethkey::{recover, public_to_address}; -#[derive(Debug, PartialEq, Eq, Clone)] +/// Message transmitted between consensus participants. +#[derive(Debug, PartialEq, Eq, Clone, Hash)] pub struct ConsensusMessage { + pub vote_step: VoteStep, + pub block_hash: Option, pub signature: H520, +} + +/// Complete step of the consensus process. +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +pub struct VoteStep { pub height: Height, pub round: Round, pub step: Step, - pub block_hash: Option, } +impl VoteStep { + pub fn new(height: Height, round: Round, step: Step) -> Self { + VoteStep { height: height, round: round, step: step } + } + + pub fn is_height(&self, height: Height) -> bool { + self.height == height + } + + pub fn is_round(&self, height: Height, round: Round) -> bool { + self.height == height && self.round == round + } +} fn consensus_round(header: &Header) -> Result { let round_rlp = header.seal().get(0).expect("seal passed basic verification; seal has 3 fields; qed"); @@ -42,53 +62,29 @@ impl ConsensusMessage { pub fn new(signature: H520, height: Height, round: Round, step: Step, block_hash: Option) -> Self { ConsensusMessage { signature: signature, - height: height, - round: round, - step: step, block_hash: block_hash, + vote_step: VoteStep::new(height, round, step), } } pub fn new_proposal(header: &Header) -> Result { Ok(ConsensusMessage { + vote_step: VoteStep::new(header.number() as Height, consensus_round(header)?, Step::Propose), signature: UntrustedRlp::new(header.seal().get(1).expect("seal passed basic verification; seal has 3 fields; qed").as_slice()).as_val()?, - height: header.number() as Height, - round: consensus_round(header)?, - step: Step::Propose, block_hash: Some(header.bare_hash()), }) } pub fn new_commit(proposal: &ConsensusMessage, signature: H520) -> Self { + let mut vote_step = proposal.vote_step.clone(); + vote_step.step = Step::Precommit; ConsensusMessage { - signature: signature, - height: proposal.height, - round: proposal.round, - step: Step::Precommit, + vote_step: vote_step, block_hash: proposal.block_hash, + signature: signature, } } - pub fn is_height(&self, height: Height) -> bool { - self.height == height - } - - pub fn is_round(&self, height: Height, round: Round) -> bool { - self.height == height && self.round == round - } - - pub fn is_step(&self, height: Height, round: Round, step: Step) -> bool { - self.height == height && self.round == round && self.step == step - } - - pub fn is_block_hash(&self, h: Height, r: Round, s: Step, block_hash: Option) -> bool { - self.height == h && self.round == r && self.step == s && self.block_hash == block_hash - } - - pub fn is_aligned(&self, m: &ConsensusMessage) -> bool { - self.is_block_hash(m.height, m.round, m.step, m.block_hash) - } - pub fn verify(&self) -> Result { let full_rlp = ::rlp::encode(self); let block_info = Rlp::new(&full_rlp).at(1); @@ -97,16 +93,30 @@ impl ConsensusMessage { } pub fn precommit_hash(&self) -> H256 { - message_info_rlp(self.height, self.round, Step::Precommit, self.block_hash).sha3() + let mut vote_step = self.vote_step.clone(); + vote_step.step = Step::Precommit; + message_info_rlp(&vote_step, self.block_hash).sha3() } } -impl PartialOrd for ConsensusMessage { - fn partial_cmp(&self, m: &ConsensusMessage) -> Option { +impl PartialOrd for VoteStep { + fn partial_cmp(&self, m: &VoteStep) -> Option { Some(self.cmp(m)) } } +impl Ord for VoteStep { + fn cmp(&self, m: &VoteStep) -> Ordering { + if self.height != m.height { + self.height.cmp(&m.height) + } else if self.round != m.round { + self.round.cmp(&m.round) + } else { + self.step.number().cmp(&m.step.number()) + } + } +} + impl Step { fn number(&self) -> u8 { match *self { @@ -118,20 +128,6 @@ impl Step { } } -impl Ord for ConsensusMessage { - fn cmp(&self, m: &ConsensusMessage) -> Ordering { - if self.height != m.height { - self.height.cmp(&m.height) - } else if self.round != m.round { - self.round.cmp(&m.round) - } else if self.step != m.step { - self.step.number().cmp(&m.step.number()) - } else { - self.signature.cmp(&m.signature) - } - } -} - impl Decodable for Step { fn decode(decoder: &D) -> Result where D: Decoder { match decoder.as_rlp().as_val()? { @@ -149,42 +145,39 @@ impl Encodable for Step { } } -/// (signature, height, round, step, block_hash) +/// (signature, (height, round, step, block_hash)) impl Decodable for ConsensusMessage { fn decode(decoder: &D) -> Result where D: Decoder { let rlp = decoder.as_rlp(); let m = rlp.at(1)?; let block_message: H256 = m.val_at(3)?; Ok(ConsensusMessage { - signature: rlp.val_at(0)?, - height: m.val_at(0)?, - round: m.val_at(1)?, - step: m.val_at(2)?, + vote_step: VoteStep::new(m.val_at(0)?, m.val_at(1)?, m.val_at(2)?), block_hash: match block_message.is_zero() { true => None, false => Some(block_message), - } + }, + signature: rlp.val_at(0)?, }) } -} +} impl Encodable for ConsensusMessage { fn rlp_append(&self, s: &mut RlpStream) { - let info = message_info_rlp(self.height, self.round, self.step, self.block_hash); + let info = message_info_rlp(&self.vote_step, self.block_hash); s.begin_list(2) .append(&self.signature) .append_raw(&info, 1); } } -pub fn message_info_rlp(height: Height, round: Round, step: Step, block_hash: Option) -> Bytes { +pub fn message_info_rlp(vote_step: &VoteStep, block_hash: Option) -> Bytes { // TODO: figure out whats wrong with nested list encoding let mut s = RlpStream::new_list(5); - s.append(&height).append(&round).append(&step).append(&block_hash.unwrap_or_else(H256::zero)); + s.append(&vote_step.height).append(&vote_step.round).append(&vote_step.step).append(&block_hash.unwrap_or_else(H256::zero)); s.out() } - pub fn message_full_rlp(signature: &H520, vote_info: &Bytes) -> Bytes { let mut s = RlpStream::new_list(2); s.append(signature).append_raw(vote_info, 1); @@ -199,14 +192,17 @@ mod tests { use super::*; use account_provider::AccountProvider; use header::Header; + use ethkey::Secret; #[test] fn encode_decode() { let message = ConsensusMessage { signature: H520::default(), - height: 10, - round: 123, - step: Step::Precommit, + vote_step: VoteStep { + height: 10, + round: 123, + step: Step::Precommit, + }, block_hash: Some("1".sha3()) }; let raw_rlp = ::rlp::encode(&message).to_vec(); @@ -215,9 +211,11 @@ mod tests { let message = ConsensusMessage { signature: H520::default(), - height: 1314, - round: 0, - step: Step::Prevote, + vote_step: VoteStep { + height: 1314, + round: 0, + step: Step::Prevote, + }, block_hash: None }; let raw_rlp = ::rlp::encode(&message); @@ -228,10 +226,10 @@ mod tests { #[test] fn generate_and_verify() { let tap = Arc::new(AccountProvider::transient_provider()); - let addr = tap.insert_account("0".sha3(), "0").unwrap(); + let addr = tap.insert_account(Secret::from_slice(&"0".sha3()).unwrap(), "0").unwrap(); tap.unlock_account_permanently(addr, "0".into()).unwrap(); - let mi = message_info_rlp(123, 2, Step::Precommit, Some(H256::default())); + let mi = message_info_rlp(&VoteStep::new(123, 2, Step::Precommit), Some(H256::default())); let raw_rlp = message_full_rlp(&tap.sign(addr, None, mi.sha3()).unwrap().into(), &mi); @@ -254,9 +252,11 @@ mod tests { message, ConsensusMessage { signature: Default::default(), - height: 0, - round: 0, - step: Step::Propose, + vote_step: VoteStep { + height: 0, + round: 0, + step: Step::Propose, + }, block_hash: Some(header.bare_hash()) } ); @@ -267,12 +267,10 @@ mod tests { let header = Header::default(); let pro = ConsensusMessage { signature: Default::default(), - height: 0, - round: 0, - step: Step::Propose, + vote_step: VoteStep::new(0, 0, Step::Propose), block_hash: Some(header.bare_hash()) }; - let pre = message_info_rlp(0, 0, Step::Precommit, Some(header.bare_hash())); + let pre = message_info_rlp(&VoteStep::new(0, 0, Step::Precommit), Some(header.bare_hash())); assert_eq!(pro.precommit_hash(), pre.sha3()); } diff --git a/ethcore/src/engines/tendermint/mod.rs b/ethcore/src/engines/tendermint/mod.rs index 5fe9a0248..9b7fa8cf6 100644 --- a/ethcore/src/engines/tendermint/mod.rs +++ b/ethcore/src/engines/tendermint/mod.rs @@ -53,7 +53,7 @@ use self::transition::TransitionHandler; use self::params::TendermintParams; use self::vote_collector::VoteCollector; -#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] pub enum Step { Propose, Prevote, @@ -163,13 +163,13 @@ impl Tendermint { let h = self.height.load(AtomicOrdering::SeqCst); let r = self.round.load(AtomicOrdering::SeqCst); let s = self.step.read(); - let vote_info = message_info_rlp(h, r, *s, block_hash); + let vote_info = message_info_rlp(&VoteStep::new(h, r, *s), block_hash); let authority = self.authority.read(); match ap.sign(*authority, self.password.read().clone(), vote_info.sha3()).map(Into::into) { Ok(signature) => { let message_rlp = message_full_rlp(&signature, &vote_info); let message = ConsensusMessage::new(signature, h, r, *s, block_hash); - self.votes.vote(message.clone(), *authority); + self.votes.vote(message.clone(), &*authority); debug!(target: "poa", "Generated {:?} as {}.", message, *authority); self.handle_valid_message(&message); @@ -221,7 +221,7 @@ impl Tendermint { }, Step::Prevote => { let block_hash = match *self.lock_change.read() { - Some(ref m) if !self.should_unlock(m.round) => m.block_hash, + Some(ref m) if !self.should_unlock(m.vote_step.round) => m.block_hash, _ => self.proposal.read().clone(), }; self.generate_and_broadcast_message(block_hash); @@ -230,8 +230,8 @@ impl Tendermint { trace!(target: "poa", "to_step: Precommit."); let block_hash = match *self.lock_change.read() { Some(ref m) if self.is_round(m) && m.block_hash.is_some() => { - trace!(target: "poa", "Setting last lock: {}", m.round); - self.last_lock.store(m.round, AtomicOrdering::SeqCst); + trace!(target: "poa", "Setting last lock: {}", m.vote_step.round); + self.last_lock.store(m.vote_step.round, AtomicOrdering::SeqCst); m.block_hash }, _ => None, @@ -246,7 +246,7 @@ impl Tendermint { if let Some(block_hash) = *self.proposal.read() { // Generate seal and remove old votes. if self.is_proposer(&*self.authority.read()).is_ok() { - if let Some(seal) = self.votes.seal_signatures(height, round, block_hash) { + if let Some(seal) = self.votes.seal_signatures(height, round, &block_hash) { trace!(target: "poa", "Collected seal: {:?}", seal); let seal = vec![ ::rlp::encode(&round).to_vec(), @@ -290,11 +290,11 @@ impl Tendermint { } fn is_height(&self, message: &ConsensusMessage) -> bool { - message.is_height(self.height.load(AtomicOrdering::SeqCst)) + message.vote_step.is_height(self.height.load(AtomicOrdering::SeqCst)) } fn is_round(&self, message: &ConsensusMessage) -> bool { - message.is_round(self.height.load(AtomicOrdering::SeqCst), self.round.load(AtomicOrdering::SeqCst)) + message.vote_step.is_round(self.height.load(AtomicOrdering::SeqCst), self.round.load(AtomicOrdering::SeqCst)) } fn increment_round(&self, n: Round) { @@ -302,20 +302,20 @@ impl Tendermint { self.round.fetch_add(n, AtomicOrdering::SeqCst); } - fn should_unlock(&self, lock_change_round: Round) -> bool { + fn should_unlock(&self, lock_change_round: Round) -> bool { self.last_lock.load(AtomicOrdering::SeqCst) < lock_change_round && lock_change_round < self.round.load(AtomicOrdering::SeqCst) } fn has_enough_any_votes(&self) -> bool { - let step_votes = self.votes.count_step_votes(self.height.load(AtomicOrdering::SeqCst), self.round.load(AtomicOrdering::SeqCst), *self.step.read()); + let step_votes = self.votes.count_step_votes(&VoteStep::new(self.height.load(AtomicOrdering::SeqCst), self.round.load(AtomicOrdering::SeqCst), *self.step.read())); self.is_above_threshold(step_votes) } - fn has_enough_future_step_votes(&self, message: &ConsensusMessage) -> bool { - if message.round > self.round.load(AtomicOrdering::SeqCst) { - let step_votes = self.votes.count_step_votes(message.height, message.round, message.step); + fn has_enough_future_step_votes(&self, vote_step: &VoteStep) -> bool { + if vote_step.round > self.round.load(AtomicOrdering::SeqCst) { + let step_votes = self.votes.count_step_votes(vote_step); self.is_above_threshold(step_votes) } else { false @@ -328,12 +328,13 @@ impl Tendermint { } fn handle_valid_message(&self, message: &ConsensusMessage) { + let ref vote_step = message.vote_step; let is_newer_than_lock = match *self.lock_change.read() { - Some(ref lock) => message > lock, + Some(ref lock) => vote_step > &lock.vote_step, None => true, }; let lock_change = is_newer_than_lock - && message.step == Step::Prevote + && vote_step.step == Step::Prevote && message.block_hash.is_some() && self.has_enough_aligned_votes(message); if lock_change { @@ -351,15 +352,15 @@ impl Tendermint { Some(Step::Commit) } }, - Step::Precommit if self.has_enough_future_step_votes(message) => { - self.increment_round(message.round - self.round.load(AtomicOrdering::SeqCst)); + Step::Precommit if self.has_enough_future_step_votes(&vote_step) => { + self.increment_round(vote_step.round - self.round.load(AtomicOrdering::SeqCst)); Some(Step::Precommit) }, // Avoid counting twice. Step::Prevote if lock_change => Some(Step::Precommit), Step::Prevote if self.has_enough_aligned_votes(message) => Some(Step::Precommit), - Step::Prevote if self.has_enough_future_step_votes(message) => { - self.increment_round(message.round - self.round.load(AtomicOrdering::SeqCst)); + Step::Prevote if self.has_enough_future_step_votes(&vote_step) => { + self.increment_round(vote_step.round - self.round.load(AtomicOrdering::SeqCst)); Some(Step::Prevote) }, _ => None, @@ -390,8 +391,8 @@ impl Engine for Tendermint { let message = ConsensusMessage::new_proposal(header).expect("Invalid header."); map![ "signature".into() => message.signature.to_string(), - "height".into() => message.height.to_string(), - "round".into() => message.round.to_string(), + "height".into() => message.vote_step.height.to_string(), + "round".into() => message.vote_step.round.to_string(), "block_hash".into() => message.block_hash.as_ref().map(ToString::to_string).unwrap_or("".into()) ] } @@ -431,11 +432,11 @@ impl Engine for Tendermint { let height = header.number() as Height; let round = self.round.load(AtomicOrdering::SeqCst); let bh = Some(header.bare_hash()); - let vote_info = message_info_rlp(height, round, Step::Propose, bh.clone()); + let vote_info = message_info_rlp(&VoteStep::new(height, round, Step::Propose), bh.clone()); if let Ok(signature) = ap.sign(*author, self.password.read().clone(), vote_info.sha3()).map(H520::from) { // Insert Propose vote. debug!(target: "poa", "Submitting proposal {} at height {} round {}.", header.bare_hash(), height, round); - self.votes.vote(ConsensusMessage::new(signature, height, round, Step::Propose, bh), *author); + self.votes.vote(ConsensusMessage::new(signature, height, round, Step::Propose, bh), author); // Remember proposal for later seal submission. *self.proposal.write() = bh; Seal::Proposal(vec![ @@ -461,9 +462,11 @@ impl Engine for Tendermint { if !self.is_authority(&sender) { Err(EngineError::NotAuthorized(sender))?; } - self.broadcast_message(rlp.as_raw().to_vec()); + if self.votes.vote(message.clone(), &sender).is_some() { + Err(EngineError::DoubleVote(sender))? + } trace!(target: "poa", "Handling a valid {:?} from {}.", message, sender); - self.votes.vote(message.clone(), sender); + self.broadcast_message(rlp.as_raw().to_vec()); self.handle_valid_message(&message); } Ok(()) @@ -502,7 +505,7 @@ impl Engine for Tendermint { } fn verify_block_unordered(&self, header: &Header, _block: Option<&[u8]>) -> Result<(), Error> { - let proposal = ConsensusMessage::new_proposal(header)?; + let proposal = ConsensusMessage::new_proposal(header)?; let proposer = proposal.verify()?; if !self.is_authority(&proposer) { Err(EngineError::NotAuthorized(proposer))? @@ -541,7 +544,7 @@ impl Engine for Tendermint { found: signatures_len }))?; } - self.is_round_proposer(proposal.height, proposal.round, &proposer)?; + self.is_round_proposer(proposal.vote_step.height, proposal.vote_step.round, &proposer)?; } Ok(()) } @@ -607,16 +610,16 @@ impl Engine for Tendermint { let proposal = ConsensusMessage::new_proposal(header).expect("block went through full verification; this Engine verifies new_proposal creation; qed"); if signatures_len != 1 { // New Commit received, skip to next height. - trace!(target: "poa", "Received a commit for height {}, round {}.", proposal.height, proposal.round); - self.to_next_height(proposal.height); + trace!(target: "poa", "Received a commit: {:?}.", proposal.vote_step); + self.to_next_height(proposal.vote_step.height); return false; } let proposer = proposal.verify().expect("block went through full verification; this Engine tries verify; qed"); - debug!(target: "poa", "Received a new proposal for height {}, round {} from {}.", proposal.height, proposal.round, proposer); + debug!(target: "poa", "Received a new proposal {:?} from {}.", proposal.vote_step, proposer); if self.is_round(&proposal) { *self.proposal.write() = proposal.block_hash.clone(); } - self.votes.vote(proposal, proposer); + self.votes.vote(proposal, &proposer); true } @@ -671,6 +674,7 @@ mod tests { use error::{Error, BlockError}; use header::Header; use env_info::EnvInfo; + use ethkey::Secret; use client::chain_notify::ChainNotify; use miner::MinerService; use tests::helpers::*; @@ -703,7 +707,7 @@ mod tests { } fn vote(engine: &Engine, signer: F, height: usize, round: usize, step: Step, block_hash: Option) -> Bytes where F: FnOnce(H256) -> Result { - let mi = message_info_rlp(height, round, step, block_hash); + let mi = message_info_rlp(&VoteStep::new(height, round, step), block_hash); let m = message_full_rlp(&signer(mi.sha3()).unwrap().into(), &mi); engine.handle_message(&m).unwrap(); m @@ -711,7 +715,7 @@ mod tests { fn proposal_seal(tap: &Arc, header: &Header, round: Round) -> Vec { let author = header.author(); - let vote_info = message_info_rlp(header.number() as Height, round, Step::Propose, Some(header.bare_hash())); + let vote_info = message_info_rlp(&VoteStep::new(header.number() as Height, round, Step::Propose), Some(header.bare_hash())); let signature = tap.sign(*author, None, vote_info.sha3()).unwrap(); vec![ ::rlp::encode(&round).to_vec(), @@ -721,7 +725,7 @@ mod tests { } fn insert_and_unlock(tap: &Arc, acc: &str) -> Address { - let addr = tap.insert_account(acc.sha3(), acc).unwrap(); + let addr = tap.insert_account(Secret::from_slice(&acc.sha3()).unwrap(), acc).unwrap(); tap.unlock_account_permanently(addr, acc.into()).unwrap(); addr } @@ -825,7 +829,7 @@ mod tests { header.set_author(proposer); let mut seal = proposal_seal(&tap, &header, 0); - let vote_info = message_info_rlp(0, 0, Step::Precommit, Some(header.bare_hash())); + let vote_info = message_info_rlp(&VoteStep::new(0, 0, Step::Precommit), Some(header.bare_hash())); let signature1 = tap.sign(proposer, None, vote_info.sha3()).unwrap(); seal[2] = ::rlp::encode(&vec![H520::from(signature1.clone())]).to_vec(); @@ -886,7 +890,7 @@ mod tests { fn relays_messages() { let (spec, tap) = setup(); let engine = spec.engine.clone(); - + let v0 = insert_and_register(&tap, engine.as_ref(), "0"); let v1 = insert_and_register(&tap, engine.as_ref(), "1"); diff --git a/ethcore/src/engines/tendermint/vote_collector.rs b/ethcore/src/engines/tendermint/vote_collector.rs index be592bc8f..758dfb5a3 100644 --- a/ethcore/src/engines/tendermint/vote_collector.rs +++ b/ethcore/src/engines/tendermint/vote_collector.rs @@ -17,13 +17,50 @@ //! Collects votes on hashes at each height and round. use util::*; -use super::message::ConsensusMessage; -use super::{Height, Round, Step}; +use super::message::*; +use super::{Height, Round, Step, BlockHash}; #[derive(Debug)] pub struct VoteCollector { /// Storing all Proposals, Prevotes and Precommits. - votes: RwLock>, + votes: RwLock>, +} + +#[derive(Debug, Default)] +struct StepCollector { + voted: HashSet
, + pub block_votes: HashMap, HashMap>, + messages: HashSet, +} + +impl StepCollector { + /// Returns Some(&Address) when validator is double voting. + fn insert<'a>(&mut self, message: ConsensusMessage, address: &'a Address) -> Option<&'a Address> { + // Do nothing when message was seen. + if self.messages.insert(message.clone()) { + if self.voted.insert(address.clone()) { + self + .block_votes + .entry(message.block_hash) + .or_insert_with(HashMap::new) + .insert(message.signature, address.clone()); + } else { + // Bad validator sent a different message. + return Some(address); + } + } + None + } + + /// Count all votes for the given block hash at this step. + fn count_block(&self, block_hash: &Option) -> usize { + self.block_votes.get(block_hash).map_or(0, HashMap::len) + } + + /// Count all votes collected for the given step. + fn count(&self) -> usize { + self.block_votes.values().map(HashMap::len).sum() + } } #[derive(Debug)] @@ -42,109 +79,105 @@ impl PartialEq for SealSignatures { impl Eq for SealSignatures {} impl VoteCollector { - pub fn new() -> VoteCollector { + pub fn new() -> Self { let mut collector = BTreeMap::new(); - // Insert dummy message to fulfill invariant: "only messages newer than the oldest are inserted". - collector.insert(ConsensusMessage { - signature: H520::default(), - height: 0, - round: 0, - step: Step::Propose, - block_hash: None - }, - Address::default()); + // Insert dummy entry to fulfill invariant: "only messages newer than the oldest are inserted". + collector.insert(VoteStep::new(0, 0, Step::Propose), Default::default()); VoteCollector { votes: RwLock::new(collector) } } /// Insert vote if it is newer than the oldest one. - pub fn vote(&self, message: ConsensusMessage, voter: Address) -> Option
{ - self.votes.write().insert(message, voter) + pub fn vote<'a>(&self, message: ConsensusMessage, voter: &'a Address) -> Option<&'a Address> { + self + .votes + .write() + .entry(message.vote_step.clone()) + .or_insert_with(Default::default) + .insert(message, voter) } + /// Checks if the message should be ignored. pub fn is_old_or_known(&self, message: &ConsensusMessage) -> bool { - self.votes.read().get(message).map_or(false, |a| { - trace!(target: "poa", "Known message from {}: {:?}.", a, message); - true - }) || { + self + .votes + .read() + .get(&message.vote_step) + .map_or(false, |c| { + let is_known = c.messages.contains(message); + if is_known { trace!(target: "poa", "Known message: {:?}.", message); } + is_known + }) + || { let guard = self.votes.read(); - let is_old = guard.keys().next().map_or(true, |oldest| message <= oldest); + let is_old = guard.keys().next().map_or(true, |oldest| message.vote_step <= *oldest); if is_old { trace!(target: "poa", "Old message {:?}.", message); } is_old } } /// Throws out messages older than message, leaves message as marker for the oldest. - pub fn throw_out_old(&self, message: &ConsensusMessage) { + pub fn throw_out_old(&self, vote_step: &VoteStep) { let mut guard = self.votes.write(); - let new_collector = guard.split_off(message); + let new_collector = guard.split_off(vote_step); *guard = new_collector; } - pub fn seal_signatures(&self, height: Height, round: Round, block_hash: H256) -> Option { - let bh = Some(block_hash); - let (proposal, votes) = { + /// Collects the signatures used to seal a block. + pub fn seal_signatures(&self, height: Height, round: Round, block_hash: &H256) -> Option { + let ref bh = Some(*block_hash); + let precommit_step = VoteStep::new(height, round, Step::Precommit); + let maybe_seal = { let guard = self.votes.read(); - let mut current_signatures = guard.keys().skip_while(|m| !m.is_block_hash(height, round, Step::Propose, bh)); - let proposal = current_signatures.next().cloned(); - let votes = current_signatures - .skip_while(|m| !m.is_block_hash(height, round, Step::Precommit, bh)) - .filter(|m| m.is_block_hash(height, round, Step::Precommit, bh)) - .cloned() - .collect::>(); - (proposal, votes) + guard + .get(&VoteStep::new(height, round, Step::Propose)) + .and_then(|c| c.block_votes.get(bh)) + .and_then(|proposals| proposals.keys().next()) + .map(|proposal| SealSignatures { + proposal: proposal.clone(), + votes: guard + .get(&precommit_step) + .and_then(|c| c.block_votes.get(bh)) + .map(|precommits| precommits.keys().cloned().collect()) + .unwrap_or_else(Vec::new), + }) + .and_then(|seal| if seal.votes.is_empty() { None } else { Some(seal) }) }; - if votes.is_empty() { - return None; + if maybe_seal.is_some() { + // Remove messages that are no longer relevant. + self.throw_out_old(&precommit_step); } - // Remove messages that are no longer relevant. - votes.last().map(|m| self.throw_out_old(m)); - let mut votes_vec: Vec<_> = votes.into_iter().map(|m| m.signature).collect(); - votes_vec.sort(); - proposal.map(|p| SealSignatures { - proposal: p.signature, - votes: votes_vec, - }) + maybe_seal } + /// Count votes which agree with the given message. pub fn count_aligned_votes(&self, message: &ConsensusMessage) -> usize { - let guard = self.votes.read(); - guard.keys() - .skip_while(|m| !m.is_aligned(message)) - // sorted by signature so might not be continuous - .filter(|m| m.is_aligned(message)) - .count() + self + .votes + .read() + .get(&message.vote_step) + .map_or(0, |m| m.count_block(&message.block_hash)) } - pub fn count_step_votes(&self, height: Height, round: Round, step: Step) -> usize { - let guard = self.votes.read(); - let current = guard.iter().skip_while(|&(m, _)| !m.is_step(height, round, step)); - let mut origins = HashSet::new(); - let mut n = 0; - for (message, origin) in current { - if message.is_step(height, round, step) { - if origins.insert(origin) { - n += 1; - } else { - warn!("count_step_votes: Authority {} has cast multiple step votes, this indicates malicious behaviour.", origin) - } - } - } - n + /// Count all votes collected for a given step. + pub fn count_step_votes(&self, vote_step: &VoteStep) -> usize { + self.votes.read().get(vote_step).map_or(0, StepCollector::count) } + /// Get all messages older than the height. pub fn get_up_to(&self, height: Height) -> Vec { let guard = self.votes.read(); guard - .keys() - .filter(|m| m.step.is_pre()) - .take_while(|m| m.height <= height) - .map(|m| ::rlp::encode(m).to_vec()) - .collect() + .iter() + .filter(|&(s, _)| s.step.is_pre()) + .take_while(|&(s, _)| s.height <= height) + .map(|(_, c)| c.messages.iter().map(|m| ::rlp::encode(m).to_vec()).collect::>()) + .fold(Vec::new(), |mut acc, mut messages| { acc.append(&mut messages); acc }) } + /// Retrieve address from which the message was sent from cache. pub fn get(&self, message: &ConsensusMessage) -> Option
{ let guard = self.votes.read(); - guard.get(message).cloned() + guard.get(&message.vote_step).and_then(|c| c.block_votes.get(&message.block_hash)).and_then(|origins| origins.get(&message.signature).cloned()) } } @@ -152,15 +185,15 @@ impl VoteCollector { mod tests { use util::*; use super::*; - use super::super::{Height, Round, BlockHash, Step}; - use super::super::message::ConsensusMessage; + use super::super::{BlockHash, Step}; + use super::super::message::*; - fn random_vote(collector: &VoteCollector, signature: H520, h: Height, r: Round, step: Step, block_hash: Option) -> Option { - full_vote(collector, signature, h, r, step, block_hash, H160::random()) + fn random_vote(collector: &VoteCollector, signature: H520, vote_step: VoteStep, block_hash: Option) -> bool { + full_vote(collector, signature, vote_step, block_hash, &H160::random()).is_none() } - fn full_vote(collector: &VoteCollector, signature: H520, h: Height, r: Round, step: Step, block_hash: Option, address: Address) -> Option { - collector.vote(ConsensusMessage { signature: signature, height: h, round: r, step: step, block_hash: block_hash }, address) + fn full_vote<'a>(collector: &VoteCollector, signature: H520, vote_step: VoteStep, block_hash: Option, address: &'a Address) -> Option<&'a Address> { + collector.vote(ConsensusMessage { signature: signature, vote_step: vote_step, block_hash: block_hash }, address) } #[test] @@ -173,68 +206,71 @@ mod tests { for _ in 0..5 { signatures.push(H520::random()); } + let propose_step = VoteStep::new(h, r, Step::Propose); + let prevote_step = VoteStep::new(h, r, Step::Prevote); + let precommit_step = VoteStep::new(h, r, Step::Precommit); // Wrong height proposal. - random_vote(&collector, signatures[4].clone(), h - 1, r, Step::Propose, bh.clone()); + random_vote(&collector, signatures[4].clone(), VoteStep::new(h - 1, r, Step::Propose), bh.clone()); // Good proposal - random_vote(&collector, signatures[0].clone(), h, r, Step::Propose, bh.clone()); + random_vote(&collector, signatures[0].clone(), propose_step.clone(), bh.clone()); // Wrong block proposal. - random_vote(&collector, signatures[0].clone(), h, r, Step::Propose, Some("0".sha3())); + random_vote(&collector, signatures[0].clone(), propose_step.clone(), Some("0".sha3())); // Wrong block precommit. - random_vote(&collector, signatures[3].clone(), h, r, Step::Precommit, Some("0".sha3())); + random_vote(&collector, signatures[3].clone(), precommit_step.clone(), Some("0".sha3())); // Wrong round proposal. - random_vote(&collector, signatures[0].clone(), h, r - 1, Step::Propose, bh.clone()); + random_vote(&collector, signatures[0].clone(), VoteStep::new(h, r - 1, Step::Propose), bh.clone()); // Prevote. - random_vote(&collector, signatures[0].clone(), h, r, Step::Prevote, bh.clone()); + random_vote(&collector, signatures[0].clone(), prevote_step.clone(), bh.clone()); // Relevant precommit. - random_vote(&collector, signatures[2].clone(), h, r, Step::Precommit, bh.clone()); + random_vote(&collector, signatures[2].clone(), precommit_step.clone(), bh.clone()); // Replcated vote. - random_vote(&collector, signatures[2].clone(), h, r, Step::Precommit, bh.clone()); + random_vote(&collector, signatures[2].clone(), precommit_step.clone(), bh.clone()); // Wrong round precommit. - random_vote(&collector, signatures[4].clone(), h, r + 1, Step::Precommit, bh.clone()); + random_vote(&collector, signatures[4].clone(), VoteStep::new(h, r + 1, Step::Precommit), bh.clone()); // Wrong height precommit. - random_vote(&collector, signatures[3].clone(), h + 1, r, Step::Precommit, bh.clone()); + random_vote(&collector, signatures[3].clone(), VoteStep::new(h + 1, r, Step::Precommit), bh.clone()); // Relevant precommit. - random_vote(&collector, signatures[1].clone(), h, r, Step::Precommit, bh.clone()); + random_vote(&collector, signatures[1].clone(), precommit_step.clone(), bh.clone()); // Wrong round precommit, same signature. - random_vote(&collector, signatures[1].clone(), h, r + 1, Step::Precommit, bh.clone()); + random_vote(&collector, signatures[1].clone(), VoteStep::new(h, r + 1, Step::Precommit), bh.clone()); // Wrong round precommit. - random_vote(&collector, signatures[4].clone(), h, r - 1, Step::Precommit, bh.clone()); + random_vote(&collector, signatures[4].clone(), VoteStep::new(h, r - 1, Step::Precommit), bh.clone()); let seal = SealSignatures { proposal: signatures[0], votes: signatures[1..3].to_vec() }; - assert_eq!(seal, collector.seal_signatures(h, r, bh.unwrap()).unwrap()); + assert_eq!(seal, collector.seal_signatures(h, r, &bh.unwrap()).unwrap()); } #[test] fn count_votes() { let collector = VoteCollector::new(); + let prevote_step = VoteStep::new(3, 2, Step::Prevote); + let precommit_step = VoteStep::new(3, 2, Step::Precommit); // good prevote - random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("0".sha3())); - random_vote(&collector, H520::random(), 3, 1, Step::Prevote, Some("0".sha3())); + random_vote(&collector, H520::random(), prevote_step.clone(), Some("0".sha3())); + random_vote(&collector, H520::random(), VoteStep::new(3, 1, Step::Prevote), Some("0".sha3())); // good precommit - random_vote(&collector, H520::random(), 3, 2, Step::Precommit, Some("0".sha3())); - random_vote(&collector, H520::random(), 3, 3, Step::Precommit, Some("0".sha3())); + random_vote(&collector, H520::random(), precommit_step.clone(), Some("0".sha3())); + random_vote(&collector, H520::random(), VoteStep::new(3, 3, Step::Precommit), Some("0".sha3())); // good prevote - random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("1".sha3())); + random_vote(&collector, H520::random(), prevote_step.clone(), Some("1".sha3())); // good prevote let same_sig = H520::random(); - random_vote(&collector, same_sig.clone(), 3, 2, Step::Prevote, Some("1".sha3())); - random_vote(&collector, same_sig, 3, 2, Step::Prevote, Some("1".sha3())); + random_vote(&collector, same_sig.clone(), prevote_step.clone(), Some("1".sha3())); + random_vote(&collector, same_sig, prevote_step.clone(), Some("1".sha3())); // good precommit - random_vote(&collector, H520::random(), 3, 2, Step::Precommit, Some("1".sha3())); + random_vote(&collector, H520::random(), precommit_step.clone(), Some("1".sha3())); // good prevote - random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("0".sha3())); - random_vote(&collector, H520::random(), 2, 2, Step::Precommit, Some("2".sha3())); + random_vote(&collector, H520::random(), prevote_step.clone(), Some("0".sha3())); + random_vote(&collector, H520::random(), VoteStep::new(2, 2, Step::Precommit), Some("2".sha3())); - assert_eq!(collector.count_step_votes(3, 2, Step::Prevote), 4); - assert_eq!(collector.count_step_votes(3, 2, Step::Precommit), 2); + assert_eq!(collector.count_step_votes(&prevote_step), 4); + assert_eq!(collector.count_step_votes(&precommit_step), 2); let message = ConsensusMessage { signature: H520::default(), - height: 3, - round: 2, - step: Step::Prevote, + vote_step: prevote_step, block_hash: Some("1".sha3()) }; assert_eq!(collector.count_aligned_votes(&message), 2); @@ -243,30 +279,29 @@ mod tests { #[test] fn remove_old() { let collector = VoteCollector::new(); - random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("0".sha3())); - random_vote(&collector, H520::random(), 3, 1, Step::Prevote, Some("0".sha3())); - random_vote(&collector, H520::random(), 3, 3, Step::Precommit, Some("0".sha3())); - random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("1".sha3())); - random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("1".sha3())); - random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("0".sha3())); - random_vote(&collector, H520::random(), 2, 2, Step::Precommit, Some("2".sha3())); - - let message = ConsensusMessage { - signature: H520::default(), - height: 3, - round: 2, - step: Step::Precommit, - block_hash: Some("1".sha3()) + let vote = |height, round, step, hash| { + random_vote(&collector, H520::random(), VoteStep::new(height, round, step), hash); }; - collector.throw_out_old(&message); + vote(3, 2, Step::Prevote, Some("0".sha3())); + vote(3, 1, Step::Prevote, Some("0".sha3())); + vote(3, 3, Step::Precommit, Some("0".sha3())); + vote(3, 2, Step::Prevote, Some("1".sha3())); + vote(3, 2, Step::Prevote, Some("1".sha3())); + vote(3, 2, Step::Prevote, Some("0".sha3())); + vote(2, 2, Step::Precommit, Some("2".sha3())); + + collector.throw_out_old(&VoteStep::new(3, 2, Step::Precommit)); assert_eq!(collector.votes.read().len(), 1); } #[test] fn malicious_authority() { let collector = VoteCollector::new(); - full_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("0".sha3()), Address::default()); - full_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("1".sha3()), Address::default()); - assert_eq!(collector.count_step_votes(3, 2, Step::Prevote), 1); + let vote_step = VoteStep::new(3, 2, Step::Prevote); + // Vote is inserted fine. + assert!(full_vote(&collector, H520::random(), vote_step.clone(), Some("0".sha3()), &Address::default()).is_none()); + // Returns the double voting address. + full_vote(&collector, H520::random(), vote_step.clone(), Some("1".sha3()), &Address::default()).unwrap(); + assert_eq!(collector.count_step_votes(&vote_step), 1); } } diff --git a/ethcore/src/engines/validator_set/contract.rs b/ethcore/src/engines/validator_set/contract.rs index b8b63112c..7efe668e6 100644 --- a/ethcore/src/engines/validator_set/contract.rs +++ b/ethcore/src/engines/validator_set/contract.rs @@ -118,17 +118,17 @@ mod provider { } } fn as_string(e: T) -> String { format!("{:?}", e) } - + /// Auto-generated from: `{"constant":true,"inputs":[],"name":"getValidators","outputs":[{"name":"","type":"address[]"}],"payable":false,"type":"function"}` #[allow(dead_code)] - pub fn get_validators(&self) -> Result, String> { + pub fn get_validators(&self) -> Result, String> { let call = self.contract.function("getValidators".into()).map_err(Self::as_string)?; let data = call.encode_call( vec![] ).map_err(Self::as_string)?; let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; let mut result = output.into_iter().rev().collect::>(); - Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_array().and_then(|v| v.into_iter().map(|a| a.to_address()).collect::>>()).ok_or("Invalid type returned")?; r.into_iter().map(|a| util::Address::from(a)).collect::>() })) + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_array().and_then(|v| v.into_iter().map(|a| a.to_address()).collect::>>()).ok_or("Invalid type returned")?; r.into_iter().map(|a| util::Address::from(a)).collect::>() })) } } } @@ -140,6 +140,7 @@ mod tests { use account_provider::AccountProvider; use transaction::{Transaction, Action}; use client::{BlockChainClient, EngineClient}; + use ethkey::Secret; use miner::MinerService; use tests::helpers::generate_dummy_client_with_spec_and_data; use super::super::ValidatorSet; @@ -158,8 +159,9 @@ mod tests { #[test] fn changes_validators() { let tap = Arc::new(AccountProvider::transient_provider()); - let v0 = tap.insert_account("1".sha3(), "").unwrap(); - let v1 = tap.insert_account("0".sha3(), "").unwrap(); + let s0 = Secret::from_slice(&"1".sha3()).unwrap(); + let v0 = tap.insert_account(s0.clone(), "").unwrap(); + let v1 = tap.insert_account(Secret::from_slice(&"0".sha3()).unwrap(), "").unwrap(); let spec_factory = || { let spec = Spec::new_validator_contract(); spec.engine.register_account_provider(tap.clone()); @@ -178,7 +180,7 @@ mod tests { action: Action::Call(validator_contract), value: 0.into(), data: "f94e18670000000000000000000000000000000000000000000000000000000000000001".from_hex().unwrap(), - }.sign(&"1".sha3(), None); + }.sign(&s0, None); client.miner().import_own_transaction(client.as_ref(), tx.into()).unwrap(); client.update_sealing(); assert_eq!(client.chain_info().best_block_number, 1); @@ -190,7 +192,7 @@ mod tests { action: Action::Call(validator_contract), value: 0.into(), data: "4d238c8e00000000000000000000000082a978b3f5962a5b0957d9ee9eef472ee55b42f1".from_hex().unwrap(), - }.sign(&"1".sha3(), None); + }.sign(&s0, None); client.miner().import_own_transaction(client.as_ref(), tx.into()).unwrap(); client.update_sealing(); // The transaction is not yet included so still unable to seal. @@ -209,7 +211,7 @@ mod tests { action: Action::Call(Address::default()), value: 0.into(), data: Vec::new(), - }.sign(&"1".sha3(), None); + }.sign(&s0, None); client.miner().import_own_transaction(client.as_ref(), tx.into()).unwrap(); client.update_sealing(); // Able to seal again. diff --git a/ethcore/src/evm/evm.rs b/ethcore/src/evm/evm.rs index 3270b06e4..78dc8b73a 100644 --- a/ethcore/src/evm/evm.rs +++ b/ethcore/src/evm/evm.rs @@ -22,7 +22,7 @@ use action_params::ActionParams; use evm::Ext; /// Evm errors. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum Error { /// `OutOfGas` is returned when transaction execution runs out of gas. /// The state should be reverted to the state from before the diff --git a/ethcore/src/executive.rs b/ethcore/src/executive.rs index 72427c668..a8c9596ab 100644 --- a/ethcore/src/executive.rs +++ b/ethcore/src/executive.rs @@ -463,8 +463,9 @@ impl<'a> Executive<'a> { match result { Err(evm::Error::Internal) => Err(ExecutionError::Internal), - Err(_) => { + Err(exception) => { Ok(Executed { + exception: Some(exception), gas: t.gas, gas_used: t.gas, refunded: U256::zero(), @@ -479,6 +480,7 @@ impl<'a> Executive<'a> { }, _ => { Ok(Executed { + exception: None, gas: t.gas, gas_used: gas_used, refunded: refunded, diff --git a/ethcore/src/state/mod.rs b/ethcore/src/state/mod.rs index cfd53053e..ae52ee3b1 100644 --- a/ethcore/src/state/mod.rs +++ b/ethcore/src/state/mod.rs @@ -844,6 +844,7 @@ mod tests { use std::str::FromStr; use rustc_serialize::hex::FromHex; use super::*; + use ethkey::Secret; use util::{U256, H256, FixedHash, Address, Hashable}; use tests::helpers::*; use devtools::*; @@ -854,6 +855,10 @@ mod tests { use trace::{FlatTrace, TraceError, trace}; use types::executed::CallType; + fn secret() -> Secret { + Secret::from_slice(&"".sha3()).unwrap() + } + #[test] fn should_apply_create_transaction() { init_log(); @@ -872,7 +877,7 @@ mod tests { action: Action::Create, value: 100.into(), data: FromHex::from_hex("601080600c6000396000f3006000355415600957005b60203560003555").unwrap(), - }.sign(&"".sha3(), None); + }.sign(&secret(), None); state.add_balance(t.sender().as_ref().unwrap(), &(100.into()), CleanupMode::NoEmpty); let result = state.apply(&info, &engine, &t, true).unwrap(); @@ -932,7 +937,7 @@ mod tests { action: Action::Create, value: 100.into(), data: FromHex::from_hex("5b600056").unwrap(), - }.sign(&"".sha3(), None); + }.sign(&secret(), None); state.add_balance(t.sender().as_ref().unwrap(), &(100.into()), CleanupMode::NoEmpty); let result = state.apply(&info, &engine, &t, true).unwrap(); @@ -969,7 +974,7 @@ mod tests { action: Action::Call(0xa.into()), value: 100.into(), data: vec![], - }.sign(&"".sha3(), None); + }.sign(&secret(), None); state.init_code(&0xa.into(), FromHex::from_hex("6000").unwrap()); state.add_balance(t.sender().as_ref().unwrap(), &(100.into()), CleanupMode::NoEmpty); @@ -1012,7 +1017,7 @@ mod tests { action: Action::Call(0xa.into()), value: 100.into(), data: vec![], - }.sign(&"".sha3(), None); + }.sign(&secret(), None); state.add_balance(t.sender().as_ref().unwrap(), &(100.into()), CleanupMode::NoEmpty); let result = state.apply(&info, &engine, &t, true).unwrap(); @@ -1054,7 +1059,7 @@ mod tests { action: Action::Call(0x1.into()), value: 0.into(), data: vec![], - }.sign(&"".sha3(), None); + }.sign(&secret(), None); let result = state.apply(&info, engine, &t, true).unwrap(); @@ -1096,7 +1101,7 @@ mod tests { action: Action::Call(0xa.into()), value: 0.into(), data: vec![], - }.sign(&"".sha3(), None); + }.sign(&secret(), None); state.init_code(&0xa.into(), FromHex::from_hex("600060006000600060006001610be0f1").unwrap()); let result = state.apply(&info, engine, &t, true).unwrap(); @@ -1139,7 +1144,7 @@ mod tests { action: Action::Call(0xa.into()), value: 0.into(), data: vec![], - }.sign(&"".sha3(), None); + }.sign(&secret(), None); state.init_code(&0xa.into(), FromHex::from_hex("60006000600060006000600b611000f2").unwrap()); state.init_code(&0xb.into(), FromHex::from_hex("6000").unwrap()); @@ -1201,7 +1206,7 @@ mod tests { action: Action::Call(0xa.into()), value: 0.into(), data: vec![], - }.sign(&"".sha3(), None); + }.sign(&secret(), None); state.init_code(&0xa.into(), FromHex::from_hex("6000600060006000600b618000f4").unwrap()); state.init_code(&0xb.into(), FromHex::from_hex("6000").unwrap()); @@ -1260,7 +1265,7 @@ mod tests { action: Action::Call(0xa.into()), value: 100.into(), data: vec![], - }.sign(&"".sha3(), None); + }.sign(&secret(), None); state.init_code(&0xa.into(), FromHex::from_hex("5b600056").unwrap()); state.add_balance(t.sender().as_ref().unwrap(), &(100.into()), CleanupMode::NoEmpty); @@ -1300,7 +1305,7 @@ mod tests { action: Action::Call(0xa.into()), value: 100.into(), data: vec![], - }.sign(&"".sha3(), None); + }.sign(&secret(), None); state.init_code(&0xa.into(), FromHex::from_hex("60006000600060006000600b602b5a03f1").unwrap()); state.init_code(&0xb.into(), FromHex::from_hex("6000").unwrap()); @@ -1360,7 +1365,7 @@ mod tests { action: Action::Call(0xa.into()), value: 100.into(), data: vec![], - }.sign(&"".sha3(), None); + }.sign(&secret(), None); state.init_code(&0xa.into(), FromHex::from_hex("60006000600060006045600b6000f1").unwrap()); state.add_balance(t.sender().as_ref().unwrap(), &(100.into()), CleanupMode::NoEmpty); @@ -1415,7 +1420,7 @@ mod tests { action: Action::Call(0xa.into()), value: 100.into(), data: vec![], - }.sign(&"".sha3(), None); + }.sign(&secret(), None); state.init_code(&0xa.into(), FromHex::from_hex("600060006000600060ff600b6000f1").unwrap()); // not enough funds. state.add_balance(t.sender().as_ref().unwrap(), &(100.into()), CleanupMode::NoEmpty); @@ -1458,7 +1463,7 @@ mod tests { action: Action::Call(0xa.into()), value: 100.into(), data: vec![],//600480600b6000396000f35b600056 - }.sign(&"".sha3(), None); + }.sign(&secret(), None); state.init_code(&0xa.into(), FromHex::from_hex("60006000600060006000600b602b5a03f1").unwrap()); state.init_code(&0xb.into(), FromHex::from_hex("5b600056").unwrap()); @@ -1514,7 +1519,7 @@ mod tests { action: Action::Call(0xa.into()), value: 100.into(), data: vec![], - }.sign(&"".sha3(), None); + }.sign(&secret(), None); state.init_code(&0xa.into(), FromHex::from_hex("60006000600060006000600b602b5a03f1").unwrap()); state.init_code(&0xb.into(), FromHex::from_hex("60006000600060006000600c602b5a03f1").unwrap()); @@ -1589,7 +1594,7 @@ mod tests { action: Action::Call(0xa.into()), value: 100.into(), data: vec![],//600480600b6000396000f35b600056 - }.sign(&"".sha3(), None); + }.sign(&secret(), None); state.init_code(&0xa.into(), FromHex::from_hex("60006000600060006000600b602b5a03f1").unwrap()); state.init_code(&0xb.into(), FromHex::from_hex("60006000600060006000600c602b5a03f1505b601256").unwrap()); @@ -1662,7 +1667,7 @@ mod tests { action: Action::Call(0xa.into()), value: 100.into(), data: vec![], - }.sign(&"".sha3(), None); + }.sign(&secret(), None); state.init_code(&0xa.into(), FromHex::from_hex("73000000000000000000000000000000000000000bff").unwrap()); state.add_balance(&0xa.into(), &50.into(), CleanupMode::NoEmpty); diff --git a/ethcore/src/tests/client.rs b/ethcore/src/tests/client.rs index 0dfd8434d..347d1adc3 100644 --- a/ethcore/src/tests/client.rs +++ b/ethcore/src/tests/client.rs @@ -28,7 +28,7 @@ use rlp::View; use spec::Spec; use views::BlockView; use util::stats::Histogram; -use ethkey::KeyPair; +use ethkey::{KeyPair, Secret}; use transaction::{PendingTransaction, Transaction, Action}; use miner::MinerService; @@ -290,7 +290,7 @@ fn change_history_size() { #[test] fn does_not_propagate_delayed_transactions() { - let key = KeyPair::from_secret("test".sha3()).unwrap(); + let key = KeyPair::from_secret(Secret::from_slice(&"test".sha3()).unwrap()).unwrap(); let secret = key.secret(); let tx0 = PendingTransaction::new(Transaction { nonce: 0.into(), diff --git a/ethcore/src/tests/helpers.rs b/ethcore/src/tests/helpers.rs index d08261306..55306dc51 100644 --- a/ethcore/src/tests/helpers.rs +++ b/ethcore/src/tests/helpers.rs @@ -163,7 +163,7 @@ pub fn generate_dummy_client_with_spec_and_data(get_test_spec: F, block_numbe let mut last_hashes = vec![]; let mut last_header = genesis_header.clone(); - let kp = KeyPair::from_secret("".sha3()).unwrap(); + let kp = KeyPair::from_secret_slice(&"".sha3()).unwrap(); let author = kp.address(); let mut n = 0; diff --git a/ethcore/src/types/executed.rs b/ethcore/src/types/executed.rs index 1f0ef33c7..ab7be891e 100644 --- a/ethcore/src/types/executed.rs +++ b/ethcore/src/types/executed.rs @@ -18,6 +18,7 @@ use util::{Bytes, U256, Address, U512}; use rlp::*; +use evm; use trace::{VMTrace, FlatTrace}; use types::log_entry::LogEntry; use types::state_diff::StateDiff; @@ -65,6 +66,9 @@ impl Decodable for CallType { #[derive(Debug, PartialEq, Clone)] #[cfg_attr(feature = "ipc", binary)] pub struct Executed { + /// True if the outer call/create resulted in an exceptional exit. + pub exception: Option, + /// Gas paid up front for execution of transaction. pub gas: U256, @@ -178,6 +182,8 @@ pub enum CallError { TransactionNotFound, /// Couldn't find requested block's state in the chain. StatePruned, + /// Couldn't find an amount of gas that didn't result in an exception. + Exceptional, /// Error executing. Execution(ExecutionError), } @@ -195,6 +201,7 @@ impl fmt::Display for CallError { let msg = match *self { TransactionNotFound => "Transaction couldn't be found in the chain".into(), StatePruned => "Couldn't find the transaction block's state in the chain".into(), + Exceptional => "An exception happened in the execution".into(), Execution(ref e) => format!("{}", e), }; diff --git a/ethcore/src/types/transaction.rs b/ethcore/src/types/transaction.rs index 8dd7391bb..1f26b156d 100644 --- a/ethcore/src/types/transaction.rs +++ b/ethcore/src/types/transaction.rs @@ -102,6 +102,7 @@ impl HeapSizeOf for Transaction { impl From for SignedTransaction { fn from(t: ethjson::state::Transaction) -> Self { let to: Option = t.to.into(); + let secret = Secret::from_slice(&t.secret.0).expect("Valid secret expected."); Transaction { nonce: t.nonce.into(), gas_price: t.gas_price.into(), @@ -112,7 +113,7 @@ impl From for SignedTransaction { }, value: t.value.into(), data: t.data.into(), - }.sign(&t.secret.into(), None) + }.sign(&secret, None) } } diff --git a/ethcrypto/src/lib.rs b/ethcrypto/src/lib.rs index 4053baa9f..ea933ea60 100644 --- a/ethcrypto/src/lib.rs +++ b/ethcrypto/src/lib.rs @@ -166,7 +166,7 @@ pub mod aes { /// ECDH functions #[cfg_attr(feature="dev", allow(similar_names))] pub mod ecdh { - use secp256k1::{ecdh, key}; + use secp256k1::{ecdh, key, Error as SecpError}; use ethkey::{Secret, Public, SECP256K1}; use Error; @@ -180,13 +180,11 @@ pub mod ecdh { }; let publ = key::PublicKey::from_slice(context, &pdata)?; - // no way to create SecretKey from raw byte array. - let sec: &key::SecretKey = unsafe { ::std::mem::transmute(secret) }; - let shared = ecdh::SharedSecret::new_raw(context, &publ, sec); + let sec = key::SecretKey::from_slice(context, &secret)?; + let shared = ecdh::SharedSecret::new_raw(context, &publ, &sec); - let mut s = Secret::default(); - s.copy_from_slice(&shared[0..32]); - Ok(s) + Secret::from_slice(&shared[0..32]) + .map_err(|_| Error::Secp(SecpError::InvalidSecretKey)) } } diff --git a/ethkey/src/brain.rs b/ethkey/src/brain.rs index 2db460812..ad194388c 100644 --- a/ethkey/src/brain.rs +++ b/ethkey/src/brain.rs @@ -15,7 +15,7 @@ // along with Parity. If not, see . use keccak::Keccak256; -use super::{KeyPair, Error, Generator}; +use super::{KeyPair, Error, Generator, Secret}; /// Simple brainwallet. pub struct Brain(String); @@ -34,13 +34,15 @@ impl Generator for Brain { let mut i = 0; loop { secret = secret.keccak256(); - + match i > 16384 { false => i += 1, true => { - let result = KeyPair::from_secret(secret.clone().into()); - if result.as_ref().ok().map_or(false, |r| r.address()[0] == 0) { - return result; + if let Ok(secret) = Secret::from_slice(&secret) { + let result = KeyPair::from_secret(secret); + if result.as_ref().ok().map_or(false, |r| r.address()[0] == 0) { + return result; + } } }, } diff --git a/ethkey/src/keypair.rs b/ethkey/src/keypair.rs index 8d6eceb9c..7fd181523 100644 --- a/ethkey/src/keypair.rs +++ b/ethkey/src/keypair.rs @@ -60,11 +60,14 @@ impl KeyPair { Ok(keypair) } + pub fn from_secret_slice(slice: &[u8]) -> Result { + Self::from_secret(Secret::from_slice(slice)?) + } + pub fn from_keypair(sec: key::SecretKey, publ: key::PublicKey) -> Self { let context = &SECP256K1; let serialized = publ.serialize_vec(context, false); - let mut secret = Secret::default(); - secret.copy_from_slice(&sec[0..32]); + let secret = Secret::from(sec); let mut public = Public::default(); public.copy_from_slice(&serialized[1..65]); diff --git a/ethkey/src/lib.rs b/ethkey/src/lib.rs index 79faf0ef9..79921fd8c 100644 --- a/ethkey/src/lib.rs +++ b/ethkey/src/lib.rs @@ -29,6 +29,7 @@ mod keccak; mod prefix; mod random; mod signature; +mod secret; lazy_static! { pub static ref SECP256K1: secp256k1::Secp256k1 = secp256k1::Secp256k1::new(); @@ -46,10 +47,10 @@ pub use self::keypair::{KeyPair, public_to_address}; pub use self::prefix::Prefix; pub use self::random::Random; pub use self::signature::{sign, verify_public, verify_address, recover, Signature}; +pub use self::secret::Secret; use bigint::hash::{H160, H256, H512}; pub type Address = H160; -pub type Secret = H256; pub type Message = H256; pub type Public = H512; diff --git a/ethkey/src/secret.rs b/ethkey/src/secret.rs new file mode 100644 index 000000000..f109abaa4 --- /dev/null +++ b/ethkey/src/secret.rs @@ -0,0 +1,69 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +use std::fmt; +use std::ops::Deref; +use std::str::FromStr; +use secp256k1::key; +use bigint::hash::H256; +use {Error}; + +#[derive(Clone, PartialEq, Eq)] +pub struct Secret { + inner: H256, +} + +impl fmt::Debug for Secret { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "Secret: 0x{:x}{:x}..{:x}{:x}", self.inner[0], self.inner[1], self.inner[30], self.inner[31]) + } +} + +impl Secret { + pub fn from_slice(key: &[u8]) -> Result { + if key.len() != 32 { + return Err(Error::InvalidSecret); + } + + let mut h = H256::default(); + h.copy_from_slice(&key[0..32]); + Ok(Secret { inner: h }) + } +} + +impl FromStr for Secret { + type Err = Error; + fn from_str(s: &str) -> Result { + let hash = H256::from_str(s).map_err(|e| Error::Custom(format!("{:?}", e)))?; + Self::from_slice(&hash) + } +} + +impl From for Secret { + fn from(key: key::SecretKey) -> Self { + Self::from_slice(&key[0..32]) + .expect("`key::SecretKey` is valid (no way to construct invalid one); qed") + } +} + +impl Deref for Secret { + type Target = H256; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + diff --git a/ethkey/src/signature.rs b/ethkey/src/signature.rs index ef28fb408..724fd125f 100644 --- a/ethkey/src/signature.rs +++ b/ethkey/src/signature.rs @@ -16,7 +16,7 @@ use std::ops::{Deref, DerefMut}; use std::cmp::PartialEq; -use std::{mem, fmt}; +use std::fmt; use std::str::FromStr; use std::hash::{Hash, Hasher}; use secp256k1::{Message as SecpMessage, RecoverableSignature, RecoveryId, Error as SecpError}; @@ -169,9 +169,8 @@ impl DerefMut for Signature { pub fn sign(secret: &Secret, message: &Message) -> Result { let context = &SECP256K1; - // no way to create from raw byte array. - let sec: &SecretKey = unsafe { mem::transmute(secret) }; - let s = context.sign_recoverable(&SecpMessage::from_slice(&message[..])?, sec)?; + let sec = SecretKey::from_slice(context, &secret)?; + let s = context.sign_recoverable(&SecpMessage::from_slice(&message[..])?, &sec)?; let (rec_id, data) = s.serialize_compact(context); let mut data_arr = [0; 65]; diff --git a/ethstore/src/account/safe_account.rs b/ethstore/src/account/safe_account.rs index 76da94021..72ea08ed5 100644 --- a/ethstore/src/account/safe_account.rs +++ b/ethstore/src/account/safe_account.rs @@ -122,16 +122,14 @@ impl Crypto { return Err(Error::InvalidPassword); } - let mut secret = Secret::default(); - match self.cipher { Cipher::Aes128Ctr(ref params) => { let from = 32 - self.ciphertext.len(); - crypto::aes::decrypt(&derived_left_bits, ¶ms.iv, &self.ciphertext, &mut (&mut *secret)[from..]) + let mut secret = [0; 32]; + crypto::aes::decrypt(&derived_left_bits, ¶ms.iv, &self.ciphertext, &mut secret[from..]); + Ok(Secret::from_slice(&secret)?) }, } - - Ok(secret) } } diff --git a/ethstore/src/presale.rs b/ethstore/src/presale.rs index b9a15aed5..45d127664 100644 --- a/ethstore/src/presale.rs +++ b/ethstore/src/presale.rs @@ -47,7 +47,7 @@ impl PresaleWallet { let len = crypto::aes::decrypt_cbc(&derived_key, &self.iv, &self.ciphertext, &mut key).map_err(|_| Error::InvalidPassword)?; let unpadded = &key[..len]; - let secret = Secret::from(unpadded.keccak256()); + let secret = Secret::from_slice(&unpadded.keccak256())?; if let Ok(kp) = KeyPair::from_secret(secret) { if kp.address() == self.address { return Ok(kp) diff --git a/ethstore/tests/api.rs b/ethstore/tests/api.rs index 6485c3347..e1a98c90a 100644 --- a/ethstore/tests/api.rs +++ b/ethstore/tests/api.rs @@ -133,9 +133,9 @@ fn secret_store_load_pat_files() { #[test] fn test_decrypting_files_with_short_ciphertext() { // 31e9d1e6d844bd3a536800ef8d8be6a9975db509, 30 - let kp1 = KeyPair::from_secret("000081c29e8142bb6a81bef5a92bda7a8328a5c85bb2f9542e76f9b0f94fc018".into()).unwrap(); + let kp1 = KeyPair::from_secret("000081c29e8142bb6a81bef5a92bda7a8328a5c85bb2f9542e76f9b0f94fc018".parse().unwrap()).unwrap(); // d1e64e5480bfaf733ba7d48712decb8227797a4e , 31 - let kp2 = KeyPair::from_secret("00fa7b3db73dc7dfdf8c5fbdb796d741e4488628c41fc4febd9160a866ba0f35".into()).unwrap(); + let kp2 = KeyPair::from_secret("00fa7b3db73dc7dfdf8c5fbdb796d741e4488628c41fc4febd9160a866ba0f35".parse().unwrap()).unwrap(); let dir = DiskDirectory::at(ciphertext_path()); let store = EthStore::open(Box::new(dir)).unwrap(); let accounts = store.accounts().unwrap(); diff --git a/js/src/api/api.js b/js/src/api/api.js index bb622ab46..1eac2d89d 100644 --- a/js/src/api/api.js +++ b/js/src/api/api.js @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +import EventEmitter from 'eventemitter3'; + import { Http, Ws } from './transport'; import Contract from './contract'; @@ -22,8 +24,10 @@ import Subscriptions from './subscriptions'; import util from './util'; import { isFunction } from './util/types'; -export default class Api { +export default class Api extends EventEmitter { constructor (transport) { + super(); + if (!transport || !isFunction(transport.execute)) { throw new Error('EthApi needs transport with execute() function defined'); } diff --git a/js/src/api/contract/contract.js b/js/src/api/contract/contract.js index 9c3b02b72..df134fbfd 100644 --- a/js/src/api/contract/contract.js +++ b/js/src/api/contract/contract.js @@ -248,18 +248,32 @@ export default class Contract { .call(callParams) .then((encoded) => func.decodeOutput(encoded)) .then((tokens) => tokens.map((token) => token.value)) - .then((returns) => returns.length === 1 ? returns[0] : returns); + .then((returns) => returns.length === 1 ? returns[0] : returns) + .catch((error) => { + console.warn(`${func.name}.call`, values, error); + throw error; + }); }; if (!func.constant) { func.postTransaction = (options, values = []) => { const _options = this._encodeOptions(func, this._addOptionsTo(options), values); - return this._api.parity.postTransaction(_options); + return this._api.parity + .postTransaction(_options) + .catch((error) => { + console.warn(`${func.name}.postTransaction`, values, error); + throw error; + }); }; func.estimateGas = (options, values = []) => { const _options = this._encodeOptions(func, this._addOptionsTo(options), values); - return this._api.eth.estimateGas(_options); + return this._api.eth + .estimateGas(_options) + .catch((error) => { + console.warn(`${func.name}.estimateGas`, values, error); + throw error; + }); }; } @@ -385,6 +399,10 @@ export default class Contract { this._subscribeToChanges(); return subscriptionId; }); + }) + .catch((error) => { + console.warn('subscribe', event, _options, error); + throw error; }); } diff --git a/js/src/api/transport/error.js b/js/src/api/transport/error.js index 5fe9aac06..512d4289e 100644 --- a/js/src/api/transport/error.js +++ b/js/src/api/transport/error.js @@ -25,6 +25,7 @@ export const ERROR_CODES = { UNKNOWN_ERROR: -32009, TRANSACTION_ERROR: -32010, EXECUTION_ERROR: -32015, + EXCEPTION_ERROR: -32016, ACCOUNT_LOCKED: -32020, PASSWORD_INVALID: -32021, ACCOUNT_ERROR: -32023, diff --git a/js/src/api/transport/http/http.js b/js/src/api/transport/http/http.js index 17d428e75..36e0ae1b7 100644 --- a/js/src/api/transport/http/http.js +++ b/js/src/api/transport/http/http.js @@ -51,14 +51,14 @@ export default class Http extends JsonRpcBase { return fetch(this._url, request) .catch((error) => { - this._connected = false; + this._setDisconnected(); throw error; }) .then((response) => { - this._connected = true; + this._setConnected(); if (response.status !== 200) { - this._connected = false; + this._setDisconnected(); this.error(JSON.stringify({ status: response.status, statusText: response.statusText })); console.error(`${method}(${JSON.stringify(params)}): ${response.status}: ${response.statusText}`); diff --git a/js/src/api/transport/http/http.spec.js b/js/src/api/transport/http/http.spec.js index d67f11307..685c6b948 100644 --- a/js/src/api/transport/http/http.spec.js +++ b/js/src/api/transport/http/http.spec.js @@ -37,6 +37,26 @@ describe('api/transport/Http', () => { }); }); + describe('transport emitter', () => { + it('emits close event', (done) => { + transport.once('close', () => { + done(); + }); + + transport.execute('eth_call'); + }); + + it('emits open event', (done) => { + mockHttp([{ method: 'eth_call', reply: { result: '' } }]); + + transport.once('open', () => { + done(); + }); + + transport.execute('eth_call'); + }); + }); + describe('transport', () => { const RESULT = ['this is some result']; diff --git a/js/src/api/transport/jsonRpcBase.js b/js/src/api/transport/jsonRpcBase.js index 76f380935..d5c1e8cb7 100644 --- a/js/src/api/transport/jsonRpcBase.js +++ b/js/src/api/transport/jsonRpcBase.js @@ -14,8 +14,12 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export default class JsonRpcBase { +import EventEmitter from 'eventemitter3'; + +export default class JsonRpcBase extends EventEmitter { constructor () { + super(); + this._id = 1; this._debug = false; this._connected = false; @@ -32,6 +36,20 @@ export default class JsonRpcBase { return json; } + _setConnected () { + if (!this._connected) { + this._connected = true; + this.emit('open'); + } + } + + _setDisconnected () { + if (this._connected) { + this._connected = false; + this.emit('close'); + } + } + get id () { return this._id; } diff --git a/js/src/api/transport/ws/ws.js b/js/src/api/transport/ws/ws.js index 591cf3062..ef739ae13 100644 --- a/js/src/api/transport/ws/ws.js +++ b/js/src/api/transport/ws/ws.js @@ -22,7 +22,7 @@ import TransportError from '../error'; /* global WebSocket */ export default class Ws extends JsonRpcBase { - constructor (url, token, connect = true) { + constructor (url, token, autoconnect = true) { super(); this._url = url; @@ -32,14 +32,14 @@ export default class Ws extends JsonRpcBase { this._connecting = false; this._connected = false; this._lastError = null; - this._autoConnect = false; + this._autoConnect = autoconnect; this._retries = 0; this._reconnectTimeoutId = null; this._connectPromise = null; this._connectPromiseFunctions = {}; - if (connect) { + if (autoconnect) { this.connect(); } } @@ -124,11 +124,8 @@ export default class Ws extends JsonRpcBase { } _onOpen = (event) => { - console.log('ws:onOpen'); - - this._connected = true; + this._setConnected(); this._connecting = false; - this._autoConnect = true; this._retries = 0; Object.keys(this._messages) @@ -142,7 +139,7 @@ export default class Ws extends JsonRpcBase { } _onClose = (event) => { - this._connected = false; + this._setDisconnected(); this._connecting = false; event.timestamp = Date.now(); @@ -209,8 +206,8 @@ export default class Ws extends JsonRpcBase { if (result.error) { this.error(event.data); - // Don't print error if request rejected... - if (!/rejected/.test(result.error.message)) { + // Don't print error if request rejected or not is not yet up... + if (!/(rejected|not yet up)/.test(result.error.message)) { console.error(`${method}(${JSON.stringify(params)}): ${result.error.code}: ${result.error.message}`); } diff --git a/js/src/api/transport/ws/ws.spec.js b/js/src/api/transport/ws/ws.spec.js index 9303803bf..019cd166e 100644 --- a/js/src/api/transport/ws/ws.spec.js +++ b/js/src/api/transport/ws/ws.spec.js @@ -21,6 +21,37 @@ describe('api/transport/Ws', () => { let transport; let scope; + describe('transport emitter', () => { + const connect = () => { + const scope = mockWs(); + const transport = new Ws(TEST_WS_URL); + + return { transport, scope }; + }; + + it('emits open event', (done) => { + const { transport, scope } = connect(); + + transport.once('open', () => { + done(); + }); + + scope.stop(); + }); + + it('emits close event', (done) => { + const { transport, scope } = connect(); + + transport.once('open', () => { + scope.server.close(); + }); + + transport.once('close', () => { + done(); + }); + }); + }); + describe('transport', () => { let result; diff --git a/js/src/config.js b/js/src/config.js index 87fedecb8..6914f2f03 100644 --- a/js/src/config.js +++ b/js/src/config.js @@ -17,12 +17,20 @@ import LogLevel from 'loglevel'; export const LOG_KEYS = { + Balances: { + key: 'balances', + desc: 'Balances fetching' + }, TransferModalStore: { - path: 'modals/Transfer/store', - desc: 'Transfer Modal MobX Store' + key: 'modalsTransferStore', + desc: 'Transfer modal MobX store' + }, + Signer: { + key: 'secureApi', + desc: 'The Signer and the Secure API' } }; export const getLogger = (LOG_KEY) => { - return LogLevel.getLogger(LOG_KEY.path); + return LogLevel.getLogger(LOG_KEY.key); }; diff --git a/js/src/modals/DeployContract/deployContract.js b/js/src/modals/DeployContract/deployContract.js index 1bda0dddf..701c689ad 100644 --- a/js/src/modals/DeployContract/deployContract.js +++ b/js/src/modals/DeployContract/deployContract.js @@ -391,6 +391,10 @@ class DeployContract extends Component { .then(([gasEst, gas]) => { this.gasStore.setEstimated(gasEst.toFixed(0)); this.gasStore.setGas(gas.toFixed(0)); + }) + .catch((error) => { + this.gasStore.setEstimatedError(); + console.warn('estimateGas', error); }); } diff --git a/js/src/modals/ExecuteContract/executeContract.js b/js/src/modals/ExecuteContract/executeContract.js index 594ad3922..689678a7c 100644 --- a/js/src/modals/ExecuteContract/executeContract.js +++ b/js/src/modals/ExecuteContract/executeContract.js @@ -20,7 +20,6 @@ import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; import { toWei } from '~/api/util/wei'; import { BusyStep, Button, CompletedStep, GasPriceEditor, IdentityIcon, Modal, TxHash, Warning } from '~/ui'; @@ -156,8 +155,7 @@ class ExecuteContract extends Component { } return ( - + ); } @@ -379,6 +377,7 @@ class ExecuteContract extends Component { this.gasStore.setGas(gas.toFixed(0)); }) .catch((error) => { + this.gasStore.setEstimatedError(); console.warn('estimateGas', error); }); } @@ -470,11 +469,7 @@ function mapStateToProps (initState, initProps) { }; } -function mapDispatchToProps (dispatch) { - return bindActionCreators({}, dispatch); -} - export default connect( mapStateToProps, - mapDispatchToProps + null )(ExecuteContract); diff --git a/js/src/modals/Transfer/store.js b/js/src/modals/Transfer/store.js index 338e1f255..9da9023f1 100644 --- a/js/src/modals/Transfer/store.js +++ b/js/src/modals/Transfer/store.js @@ -23,7 +23,7 @@ import { bytesToHex } from '~/api/util/format'; import Contract from '~/api/contract'; import ERRORS from './errors'; import { ERROR_CODES } from '~/api/transport/error'; -import { DEFAULT_GAS, MAX_GAS_ESTIMATION } from '~/util/constants'; +import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants'; import GasPriceStore from '~/ui/GasPriceEditor/store'; import { getLogger, LOG_KEYS } from '~/config'; @@ -357,6 +357,7 @@ export default class TransferStore { }); }) .catch((error) => { + this.gasStore.setEstimatedError(); console.warn('etimateGas', error); this.recalculate(redo); }); @@ -441,6 +442,8 @@ export default class TransferStore { const gasTotal = new BigNumber(_gasTotal || 0); const { valueAll, isEth, isWallet } = this; + log.debug('@getValues', 'gas', gasTotal.toFormat()); + if (!valueAll) { const value = this.getTokenValue(); @@ -568,6 +571,7 @@ export default class TransferStore { send () { const { options, values } = this._getTransferParams(); options.minBlock = new BigNumber(this.minBlock || 0).gt(0) ? this.minBlock : null; + log.debug('@send', 'transfer value', options.value && options.value.toFormat()); return this._getTransferMethod().postTransaction(options, values); } @@ -626,7 +630,8 @@ export default class TransferStore { options.gas = MAX_GAS_ESTIMATION; } - const { token } = this.getValues(options.gas); + const gasTotal = new BigNumber(options.gas || DEFAULT_GAS).mul(options.gasPrice || DEFAULT_GASPRICE); + const { token } = this.getValues(gasTotal); if (isEth && !isWallet && !forceToken) { options.value = token; diff --git a/js/src/redux/providers/balances.js b/js/src/redux/providers/balances.js index 8d46e42d2..bb5ed19fa 100644 --- a/js/src/redux/providers/balances.js +++ b/js/src/redux/providers/balances.js @@ -21,51 +21,164 @@ import { padRight } from '~/api/util/format'; import Contracts from '~/contracts'; +let instance = null; + export default class Balances { constructor (store, api) { this._api = api; this._store = store; - this._tokenregSubId = null; - this._tokenregMetaSubId = null; + this._tokenreg = null; + this._tokenregSID = null; + this._tokenMetaSID = null; - // Throttled `retrieveTokens` function + this._blockNumberSID = null; + this._accountsInfoSID = null; + + // Throtthled load tokens (no more than once + // every minute) + this.loadTokens = throttle( + this._loadTokens, + 60 * 1000, + { leading: true, trailing: true } + ); + + // Throttled `_fetchBalances` function // that gets called max once every 40s this.longThrottledFetch = throttle( - this.fetchBalances, + this._fetchBalances, 40 * 1000, - { trailing: true } + { leading: false, trailing: true } ); this.shortThrottledFetch = throttle( - this.fetchBalances, + this._fetchBalances, 2 * 1000, - { trailing: true } + { leading: false, trailing: true } ); // Fetch all tokens every 2 minutes this.throttledTokensFetch = throttle( - this.fetchTokens, - 60 * 1000, - { trailing: true } + this._fetchTokens, + 2 * 60 * 1000, + { leading: false, trailing: true } ); + + // Unsubscribe previous instance if it exists + if (instance) { + Balances.stop(); + } } - start () { - this.subscribeBlockNumber(); - this.subscribeAccountsInfo(); + static get (store = {}) { + if (!instance && store) { + const { api } = store.getState(); + return Balances.instantiate(store, api); + } - this.loadTokens(); + return instance; + } + + static instantiate (store, api) { + if (!instance) { + instance = new Balances(store, api); + } + + return instance; + } + + static start () { + if (!instance) { + return Promise.reject('BalancesProvider has not been intiated yet'); + } + + const self = instance; + + // Unsubscribe from previous subscriptions + return Balances + .stop() + .then(() => self.loadTokens()) + .then(() => { + const promises = [ + self.subscribeBlockNumber(), + self.subscribeAccountsInfo() + ]; + + return Promise.all(promises); + }); + } + + static stop () { + if (!instance) { + return Promise.resolve(); + } + + const self = instance; + const promises = []; + + if (self._blockNumberSID) { + const p = self._api + .unsubscribe(self._blockNumberSID) + .then(() => { + self._blockNumberSID = null; + }); + + promises.push(p); + } + + if (self._accountsInfoSID) { + const p = self._api + .unsubscribe(self._accountsInfoSID) + .then(() => { + self._accountsInfoSID = null; + }); + + promises.push(p); + } + + // Unsubscribe without adding the promises + // to the result, since it would have to wait for a + // reconnection to resolve if the Node is disconnected + if (self._tokenreg) { + if (self._tokenregSID) { + const tokenregSID = self._tokenregSID; + + self._tokenreg + .unsubscribe(tokenregSID) + .then(() => { + if (self._tokenregSID === tokenregSID) { + self._tokenregSID = null; + } + }); + } + + if (self._tokenMetaSID) { + const tokenMetaSID = self._tokenMetaSID; + + self._tokenreg + .unsubscribe(tokenMetaSID) + .then(() => { + if (self._tokenMetaSID === tokenMetaSID) { + self._tokenMetaSID = null; + } + }); + } + } + + return Promise.all(promises); } subscribeAccountsInfo () { - this._api + return this._api .subscribe('parity_allAccountsInfo', (error, accountsInfo) => { if (error) { return; } - this.fetchBalances(); + this.fetchAllBalances(); + }) + .then((accountsInfoSID) => { + this._accountsInfoSID = accountsInfoSID; }) .catch((error) => { console.warn('_subscribeAccountsInfo', error); @@ -73,49 +186,97 @@ export default class Balances { } subscribeBlockNumber () { - this._api + return this._api .subscribe('eth_blockNumber', (error) => { if (error) { return console.warn('_subscribeBlockNumber', error); } - const { syncing } = this._store.getState().nodeStatus; - - this.throttledTokensFetch(); - - // If syncing, only retrieve balances once every - // few seconds - if (syncing) { - this.shortThrottledFetch.cancel(); - return this.longThrottledFetch(); - } - - this.longThrottledFetch.cancel(); - return this.shortThrottledFetch(); + return this.fetchAllBalances(); + }) + .then((blockNumberSID) => { + this._blockNumberSID = blockNumberSID; }) .catch((error) => { console.warn('_subscribeBlockNumber', error); }); } - fetchBalances () { - this._store.dispatch(fetchBalances()); + fetchAllBalances (options = {}) { + // If it's a network change, reload the tokens + // ( and then fetch the tokens balances ) and fetch + // the accounts balances + if (options.changedNetwork) { + this.loadTokens({ skipNotifications: true }); + this.loadTokens.flush(); + + this.fetchBalances({ + force: true, + skipNotifications: true + }); + + return; + } + + this.fetchTokensBalances(options); + this.fetchBalances(options); } - fetchTokens () { - this._store.dispatch(fetchTokensBalances()); + fetchTokensBalances (options) { + const { skipNotifications = false, force = false } = options; + + this.throttledTokensFetch(skipNotifications); + + if (force) { + this.throttledTokensFetch.flush(); + } + } + + fetchBalances (options) { + const { skipNotifications = false, force = false } = options; + const { syncing } = this._store.getState().nodeStatus; + + // If syncing, only retrieve balances once every + // few seconds + if (syncing) { + this.shortThrottledFetch.cancel(); + this.longThrottledFetch(skipNotifications); + + if (force) { + this.longThrottledFetch.flush(); + } + + return; + } + + this.longThrottledFetch.cancel(); + this.shortThrottledFetch(skipNotifications); + + if (force) { + this.shortThrottledFetch.flush(); + } + } + + _fetchBalances (skipNotifications = false) { + this._store.dispatch(fetchBalances(null, skipNotifications)); + } + + _fetchTokens (skipNotifications = false) { + this._store.dispatch(fetchTokensBalances(null, null, skipNotifications)); } getTokenRegistry () { return Contracts.get().tokenReg.getContract(); } - loadTokens () { - this + _loadTokens (options = {}) { + return this .getTokenRegistry() .then((tokenreg) => { + this._tokenreg = tokenreg; + this._store.dispatch(setTokenReg(tokenreg)); - this._store.dispatch(loadTokens()); + this._store.dispatch(loadTokens(options)); return this.attachToTokens(tokenreg); }) @@ -133,7 +294,7 @@ export default class Balances { } attachToNewToken (tokenreg) { - if (this._tokenregSubId) { + if (this._tokenregSID) { return Promise.resolve(); } @@ -149,13 +310,13 @@ export default class Balances { this.handleTokensLogs(logs); }) - .then((tokenregSubId) => { - this._tokenregSubId = tokenregSubId; + .then((tokenregSID) => { + this._tokenregSID = tokenregSID; }); } attachToTokenMetaChange (tokenreg) { - if (this._tokenregMetaSubId) { + if (this._tokenMetaSID) { return Promise.resolve(); } @@ -172,8 +333,8 @@ export default class Balances { this.handleTokensLogs(logs); }) - .then((tokenregMetaSubId) => { - this._tokenregMetaSubId = tokenregMetaSubId; + .then((tokenMetaSID) => { + this._tokenMetaSID = tokenMetaSID; }); } diff --git a/js/src/redux/providers/balancesActions.js b/js/src/redux/providers/balancesActions.js index 56a2ebafd..932ddfb8d 100644 --- a/js/src/redux/providers/balancesActions.js +++ b/js/src/redux/providers/balancesActions.js @@ -23,18 +23,27 @@ import { setAddressImage } from './imagesActions'; import * as ABIS from '~/contracts/abi'; import { notifyTransaction } from '~/util/notifications'; +import { LOG_KEYS, getLogger } from '~/config'; import imagesEthereum from '../../../assets/images/contracts/ethereum-black-64x64.png'; +const log = getLogger(LOG_KEYS.Balances); + const ETH = { name: 'Ethereum', tag: 'ETH', image: imagesEthereum }; -function setBalances (_balances) { +function setBalances (_balances, skipNotifications = false) { return (dispatch, getState) => { const state = getState(); + const currentTokens = Object.values(state.balances.tokens || {}); + const currentTags = [ 'eth' ] + .concat(currentTokens.map((token) => token.tag)) + .filter((tag) => tag) + .map((tag) => tag.toLowerCase()); + const accounts = state.personal.accounts; const nextBalances = _balances; const prevBalances = state.balances.balances; @@ -48,38 +57,55 @@ function setBalances (_balances) { const balance = Object.assign({}, balances[address]); const { tokens, txCount = balance.txCount } = nextBalances[address]; - const nextTokens = balance.tokens.slice(); - tokens.forEach((t) => { - const { token, value } = t; - const { tag } = token; + const prevTokens = balance.tokens.slice(); + const nextTokens = []; - const tokenIndex = nextTokens.findIndex((tok) => tok.token.tag === tag); + currentTags + .forEach((tag) => { + const prevToken = prevTokens.find((tok) => tok.token.tag.toLowerCase() === tag); + const nextToken = tokens.find((tok) => tok.token.tag.toLowerCase() === tag); - if (tokenIndex === -1) { - nextTokens.push({ - token, - value - }); - } else { - const oldValue = nextTokens[tokenIndex].value; + // If the given token is not in the current tokens, skip + if (!nextToken && !prevToken) { + return false; + } + + // No updates + if (!nextToken) { + return nextTokens.push(prevToken); + } + + const { token, value } = nextToken; + + // If it's a new token, push it + if (!prevToken) { + return nextTokens.push({ + token, value + }); + } + + // Otherwise, update the value + const prevValue = prevToken.value; // If received a token/eth (old value < new value), notify - if (oldValue.lt(value) && accounts[address]) { + if (prevValue.lt(value) && accounts[address] && !skipNotifications) { const account = accounts[address]; - const txValue = value.minus(oldValue); + const txValue = value.minus(prevValue); const redirectToAccount = () => { - const route = `/account/${account.address}`; + const route = `/accounts/${account.address}`; dispatch(push(route)); }; notifyTransaction(account, token, txValue, redirectToAccount); } - nextTokens[tokenIndex] = { token, value }; - } - }); + return nextTokens.push({ + ...prevToken, + value + }); + }); balances[address] = { txCount: txCount || new BigNumber(0), tokens: nextTokens }; }); @@ -123,7 +149,9 @@ export function setTokenImage (tokenAddress, image) { }; } -export function loadTokens () { +export function loadTokens (options = {}) { + log.debug('loading tokens', Object.keys(options).length ? options : ''); + return (dispatch, getState) => { const { tokenreg } = getState().balances; @@ -131,7 +159,7 @@ export function loadTokens () { .call() .then((numTokens) => { const tokenIds = range(numTokens.toNumber()); - dispatch(fetchTokens(tokenIds)); + dispatch(fetchTokens(tokenIds, options)); }) .catch((error) => { console.warn('balances::loadTokens', error); @@ -139,8 +167,9 @@ export function loadTokens () { }; } -export function fetchTokens (_tokenIds) { +export function fetchTokens (_tokenIds, options = {}) { const tokenIds = uniq(_tokenIds || []); + return (dispatch, getState) => { const { api, images, balances } = getState(); const { tokenreg } = balances; @@ -161,8 +190,9 @@ export function fetchTokens (_tokenIds) { dispatch(setAddressImage(address, image, true)); }); + log.debug('fetched token', tokens); dispatch(setTokens(tokens)); - dispatch(fetchBalances()); + dispatch(updateTokensFilter(null, null, options)); }) .catch((error) => { console.warn('balances::fetchTokens', error); @@ -170,7 +200,7 @@ export function fetchTokens (_tokenIds) { }; } -export function fetchBalances (_addresses) { +export function fetchBalances (_addresses, skipNotifications = false) { return (dispatch, getState) => { const { api, personal } = getState(); const { visibleAccounts, accounts } = personal; @@ -192,8 +222,7 @@ export function fetchBalances (_addresses) { balances[addr] = accountsBalances[idx]; }); - dispatch(setBalances(balances)); - updateTokensFilter(addresses)(dispatch, getState); + dispatch(setBalances(balances, skipNotifications)); }) .catch((error) => { console.warn('balances::fetchBalances', error); @@ -201,7 +230,7 @@ export function fetchBalances (_addresses) { }; } -export function updateTokensFilter (_addresses, _tokens) { +export function updateTokensFilter (_addresses, _tokens, options = {}) { return (dispatch, getState) => { const { api, balances, personal } = getState(); const { visibleAccounts, accounts } = personal; @@ -214,27 +243,32 @@ export function updateTokensFilter (_addresses, _tokens) { const tokenAddresses = tokens.map((t) => t.address).sort(); if (tokensFilter.filterFromId || tokensFilter.filterToId) { + // Has the tokens addresses changed (eg. a network change) const sameTokens = isEqual(tokenAddresses, tokensFilter.tokenAddresses); - const sameAddresses = isEqual(addresses, tokensFilter.addresses); - if (sameTokens && sameAddresses) { + // Addresses that are not in the current filter (omit those + // that the filter includes) + const newAddresses = addresses.filter((address) => !tokensFilter.addresses.includes(address)); + + // If no new addresses and the same tokens, don't change the filter + if (sameTokens && newAddresses.length === 0) { + log.debug('no need to update token filter', addresses, tokenAddresses, tokensFilter); return queryTokensFilter(tokensFilter)(dispatch, getState); } } - let promise = Promise.resolve(); + log.debug('updating the token filter', addresses, tokenAddresses); + const promises = []; if (tokensFilter.filterFromId) { - promise = promise.then(() => api.eth.uninstallFilter(tokensFilter.filterFromId)); + promises.push(api.eth.uninstallFilter(tokensFilter.filterFromId)); } if (tokensFilter.filterToId) { - promise = promise.then(() => api.eth.uninstallFilter(tokensFilter.filterToId)); + promises.push(api.eth.uninstallFilter(tokensFilter.filterToId)); } - if (tokenAddresses.length === 0 || addresses.length === 0) { - return promise; - } + const promise = Promise.all(promises); const TRANSFER_SIGNATURE = api.util.sha3('Transfer(address,address,uint256)'); const topicsFrom = [ TRANSFER_SIGNATURE, addresses, null ]; @@ -269,8 +303,10 @@ export function updateTokensFilter (_addresses, _tokens) { addresses, tokenAddresses }; + const { skipNotifications } = options; + dispatch(setTokensFilter(nextTokensFilter)); - fetchTokensBalances(addresses, tokens)(dispatch, getState); + fetchTokensBalances(addresses, tokens, skipNotifications)(dispatch, getState); }) .catch((error) => { console.warn('balances::updateTokensFilter', error); @@ -326,7 +362,7 @@ export function queryTokensFilter (tokensFilter) { }; } -export function fetchTokensBalances (_addresses = null, _tokens = null) { +export function fetchTokensBalances (_addresses = null, _tokens = null, skipNotifications = false) { return (dispatch, getState) => { const { api, personal, balances } = getState(); const { visibleAccounts, accounts } = personal; @@ -348,7 +384,7 @@ export function fetchTokensBalances (_addresses = null, _tokens = null) { balances[addr] = tokensBalances[idx]; }); - dispatch(setBalances(balances)); + dispatch(setBalances(balances, skipNotifications)); }) .catch((error) => { console.warn('balances::fetchTokensBalances', error); diff --git a/js/src/redux/providers/chainMiddleware.js b/js/src/redux/providers/chainMiddleware.js index 77c757da6..62c10bcb2 100644 --- a/js/src/redux/providers/chainMiddleware.js +++ b/js/src/redux/providers/chainMiddleware.js @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +import BalancesProvider from './balances'; import { showSnackbar } from './snackbarActions'; import { DEFAULT_NETCHAIN } from './statusReducer'; @@ -29,6 +30,11 @@ export default class ChainMiddleware { if (newChain !== nodeStatus.netChain && nodeStatus.netChain !== DEFAULT_NETCHAIN) { store.dispatch(showSnackbar(`Switched to ${newChain}. Please reload the page.`, 60000)); + + // Fetch the new balances without notifying the user of any change + BalancesProvider.get(store).fetchAllBalances({ + changedNetwork: true + }); } } } diff --git a/js/src/redux/providers/chainMiddleware.spec.js b/js/src/redux/providers/chainMiddleware.spec.js index ed2d5eca6..2c0a51602 100644 --- a/js/src/redux/providers/chainMiddleware.spec.js +++ b/js/src/redux/providers/chainMiddleware.spec.js @@ -16,13 +16,18 @@ import sinon from 'sinon'; +import Contracts from '~/contracts'; import { initialState as defaultNodeStatusState } from './statusReducer'; import ChainMiddleware from './chainMiddleware'; +import { createWsApi } from '~/../test/e2e/ethapi'; let middleware; let next; let store; +const api = createWsApi(); +Contracts.create(api); + function createMiddleware (collection = {}) { middleware = new ChainMiddleware().toMiddleware(); next = sinon.stub(); @@ -30,6 +35,7 @@ function createMiddleware (collection = {}) { dispatch: sinon.stub(), getState: () => { return { + api: api, nodeStatus: Object.assign({}, defaultNodeStatusState, collection) }; } diff --git a/js/src/redux/providers/personalActions.js b/js/src/redux/providers/personalActions.js index 5d91aeef8..27cc47c27 100644 --- a/js/src/redux/providers/personalActions.js +++ b/js/src/redux/providers/personalActions.js @@ -16,7 +16,8 @@ import { isEqual, intersection } from 'lodash'; -import { fetchBalances } from './balancesActions'; +import BalancesProvider from './balances'; +import { updateTokensFilter } from './balancesActions'; import { attachWallets } from './walletActions'; import Contract from '~/api/contract'; @@ -75,6 +76,9 @@ export function personalAccountsInfo (accountsInfo) { return wallet; }); }) + .catch(() => { + return []; + }) .then((_wallets) => { _wallets.forEach((wallet) => { const owners = wallet.owners.map((o) => o.address); @@ -95,7 +99,14 @@ export function personalAccountsInfo (accountsInfo) { dispatch(_personalAccountsInfo(data)); dispatch(attachWallets(wallets)); - dispatch(fetchBalances()); + + BalancesProvider.get().fetchAllBalances({ + force: true + }); + }) + .catch((error) => { + console.warn('personalAccountsInfo', error); + throw error; }); }; } @@ -123,6 +134,18 @@ export function setVisibleAccounts (addresses) { } dispatch(_setVisibleAccounts(addresses)); - dispatch(fetchBalances(addresses)); + + // Don't update the balances if no new addresses displayed + if (addresses.length === 0) { + return; + } + + // Update the Tokens filter to take into account the new + // addresses + dispatch(updateTokensFilter()); + + BalancesProvider.get().fetchBalances({ + force: true + }); }; } diff --git a/js/src/redux/providers/status.js b/js/src/redux/providers/status.js index 6d0e24c6b..e419d78f8 100644 --- a/js/src/redux/providers/status.js +++ b/js/src/redux/providers/status.js @@ -14,9 +14,15 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +import BalancesProvider from './balances'; import { statusBlockNumber, statusCollection, statusLogs } from './statusActions'; import { isEqual } from 'lodash'; +import { LOG_KEYS, getLogger } from '~/config'; + +const log = getLogger(LOG_KEYS.Signer); +let instance = null; + export default class Status { constructor (store, api) { this._api = api; @@ -27,20 +33,90 @@ export default class Status { this._longStatus = {}; this._minerSettings = {}; - this._longStatusTimeoutId = null; + this._timeoutIds = {}; + this._blockNumberSubscriptionId = null; this._timestamp = Date.now(); + + // On connecting, stop all subscriptions + api.on('connecting', this.stop, this); + + // On connected, start the subscriptions + api.on('connected', this.start, this); + + // On disconnected, stop all subscriptions + api.on('disconnected', this.stop, this); + + this.updateApiStatus(); + } + + static instantiate (store, api) { + if (!instance) { + instance = new Status(store, api); + } + + return instance; } start () { - this._subscribeBlockNumber(); - this._pollStatus(); - this._pollLongStatus(); - this._pollLogs(); + log.debug('status::start'); + + Promise + .all([ + this._subscribeBlockNumber(), + + this._pollLogs(), + this._pollLongStatus(), + this._pollStatus() + ]) + .then(() => { + return BalancesProvider.start(); + }); } - _subscribeBlockNumber () { - this._api + stop () { + log.debug('status::stop'); + + const promises = []; + + if (this._blockNumberSubscriptionId) { + const promise = this._api + .unsubscribe(this._blockNumberSubscriptionId) + .then(() => { + this._blockNumberSubscriptionId = null; + }); + + promises.push(promise); + } + + Object.values(this._timeoutIds).forEach((timeoutId) => { + clearTimeout(timeoutId); + }); + + const promise = BalancesProvider.stop(); + promises.push(promise); + + return Promise.all(promises) + .then(() => true) + .catch((error) => { + console.error('status::stop', error); + return true; + }) + .then(() => this.updateApiStatus()); + } + + updateApiStatus () { + const apiStatus = this.getApiStatus(); + log.debug('status::updateApiStatus', apiStatus); + + if (!isEqual(apiStatus, this._apiStatus)) { + this._store.dispatch(statusCollection(apiStatus)); + this._apiStatus = apiStatus; + } + } + + _subscribeBlockNumber = () => { + return this._api .subscribe('eth_blockNumber', (error, blockNumber) => { if (error) { return; @@ -51,6 +127,10 @@ export default class Status { this._api.eth .getBlockByNumber(blockNumber) .then((block) => { + if (!block) { + return; + } + this._store.dispatch(statusCollection({ blockTimestamp: block.timestamp, gasLimit: block.gasLimit @@ -59,6 +139,9 @@ export default class Status { .catch((error) => { console.warn('status._subscribeBlockNumber', 'getBlockByNumber', error); }); + }) + .then((blockNumberSubscriptionId) => { + this._blockNumberSubscriptionId = blockNumberSubscriptionId; }); } @@ -72,11 +155,7 @@ export default class Status { .catch(() => false); } - _pollStatus = () => { - const nextTimeout = (timeout = 1000) => { - setTimeout(() => this._pollStatus(), timeout); - }; - + getApiStatus = () => { const { isConnected, isConnecting, needsToken, secureToken } = this._api; const apiStatus = { @@ -86,19 +165,23 @@ export default class Status { secureToken }; - const gotConnected = !this._apiStatus.isConnected && apiStatus.isConnected; + return apiStatus; + } - if (gotConnected) { - this._pollLongStatus(); - } + _pollStatus = () => { + const nextTimeout = (timeout = 1000) => { + if (this._timeoutIds.status) { + clearTimeout(this._timeoutIds.status); + } - if (!isEqual(apiStatus, this._apiStatus)) { - this._store.dispatch(statusCollection(apiStatus)); - this._apiStatus = apiStatus; - } + this._timeoutIds.status = setTimeout(() => this._pollStatus(), timeout); + }; - if (!isConnected) { - return nextTimeout(250); + this.updateApiStatus(); + + if (!this._api.isConnected) { + nextTimeout(250); + return Promise.resolve(); } const { refreshStatus } = this._store.getState().nodeStatus; @@ -110,7 +193,7 @@ export default class Status { statusPromises.push(this._api.eth.hashrate()); } - Promise + return Promise .all(statusPromises) .then(([ syncing, ...statusResults ]) => { const status = statusResults.length === 0 @@ -125,11 +208,11 @@ export default class Status { this._store.dispatch(statusCollection(status)); this._status = status; } - - nextTimeout(); }) .catch((error) => { console.error('_pollStatus', error); + }) + .then(() => { nextTimeout(); }); } @@ -140,7 +223,7 @@ export default class Status { * from the UI */ _pollMinerSettings = () => { - Promise + return Promise .all([ this._api.eth.coinbase(), this._api.parity.extraData(), @@ -175,21 +258,21 @@ export default class Status { */ _pollLongStatus = () => { if (!this._api.isConnected) { - return; + return Promise.resolve(); } const nextTimeout = (timeout = 30000) => { - if (this._longStatusTimeoutId) { - clearTimeout(this._longStatusTimeoutId); + if (this._timeoutIds.longStatus) { + clearTimeout(this._timeoutIds.longStatus); } - this._longStatusTimeoutId = setTimeout(this._pollLongStatus, timeout); + this._timeoutIds.longStatus = setTimeout(() => this._pollLongStatus(), timeout); }; // Poll Miner settings just in case - this._pollMinerSettings(); + const minerPromise = this._pollMinerSettings(); - Promise + const mainPromise = Promise .all([ this._api.parity.netPeers(), this._api.web3.clientVersion(), @@ -225,21 +308,31 @@ export default class Status { }) .catch((error) => { console.error('_pollLongStatus', error); + }) + .then(() => { + nextTimeout(60000); }); - nextTimeout(60000); + return Promise.all([ minerPromise, mainPromise ]); } _pollLogs = () => { - const nextTimeout = (timeout = 1000) => setTimeout(this._pollLogs, timeout); + const nextTimeout = (timeout = 1000) => { + if (this._timeoutIds.logs) { + clearTimeout(this._timeoutIds.logs); + } + + this._timeoutIds.logs = setTimeout(this._pollLogs, timeout); + }; + const { devLogsEnabled } = this._store.getState().nodeStatus; if (!devLogsEnabled) { nextTimeout(); - return; + return Promise.resolve(); } - Promise + return Promise .all([ this._api.parity.devLogs(), this._api.parity.devLogsLevels() @@ -249,11 +342,12 @@ export default class Status { devLogs: devLogs.slice(-1024), devLogsLevels })); - nextTimeout(); }) .catch((error) => { console.error('_pollLogs', error); - nextTimeout(); + }) + .then(() => { + return nextTimeout(); }); } } diff --git a/js/src/redux/store.js b/js/src/redux/store.js index 9924aa461..132375784 100644 --- a/js/src/redux/store.js +++ b/js/src/redux/store.js @@ -38,10 +38,10 @@ export default function (api, browserHistory) { const middleware = initMiddleware(api, browserHistory); const store = applyMiddleware(...middleware)(storeCreation)(reducers); - new BalancesProvider(store, api).start(); + BalancesProvider.instantiate(store, api); + StatusProvider.instantiate(store, api); new PersonalProvider(store, api).start(); new SignerProvider(store, api).start(); - new StatusProvider(store, api).start(); store.dispatch(loadWallet(api)); setupWorker(store); diff --git a/js/src/secureApi.js b/js/src/secureApi.js index 445151c69..b39ecaf3b 100644 --- a/js/src/secureApi.js +++ b/js/src/secureApi.js @@ -17,133 +17,34 @@ import { uniq } from 'lodash'; import Api from './api'; +import { LOG_KEYS, getLogger } from '~/config'; +const log = getLogger(LOG_KEYS.Signer); const sysuiToken = window.localStorage.getItem('sysuiToken'); export default class SecureApi extends Api { + _isConnecting = false; + _needsToken = false; + _tokens = []; + + _dappsInterface = null; + _dappsPort = 8080; + _signerPort = 8180; + constructor (url, nextToken) { - super(new Api.Transport.Ws(url, sysuiToken, false)); + const transport = new Api.Transport.Ws(url, sysuiToken, false); + super(transport); this._url = url; - this._isConnecting = true; - this._needsToken = false; - this._dappsPort = 8080; - this._dappsInterface = null; - this._signerPort = 8180; - - // Try tokens from localstorage, then from hash + // Try tokens from localstorage, from hash and 'initial' this._tokens = uniq([sysuiToken, nextToken, 'initial']) .filter((token) => token) .map((token) => ({ value: token, tried: false })); - this._tryNextToken(); - } - - saveToken = () => { - window.localStorage.setItem('sysuiToken', this._transport.token); - // DEBUG: console.log('SecureApi:saveToken', this._transport.token); - } - - /** - * Returns a Promise that gets resolved with - * a boolean: `true` if the node is up, `false` - * otherwise - */ - _checkNodeUp () { - const url = this._url.replace(/wss?/, 'http'); - return fetch(url, { method: 'HEAD' }) - .then( - (r) => r.status === 200, - () => false - ) - .catch(() => false); - } - - _setManual () { - this._needsToken = true; - this._isConnecting = false; - } - - _tryNextToken () { - const nextTokenIndex = this._tokens.findIndex((t) => !t.tried); - - if (nextTokenIndex < 0) { - return this._setManual(); - } - - const nextToken = this._tokens[nextTokenIndex]; - nextToken.tried = true; - - this.updateToken(nextToken.value); - } - - _followConnection = () => { - const token = this.transport.token; - - return this - .transport - .connect() - .then(() => { - if (token === 'initial') { - return this.signer - .generateAuthorizationToken() - .then((token) => { - return this.updateToken(token); - }) - .catch((e) => console.error(e)); - } - - this.connectSuccess(); - return true; - }) - .catch((e) => { - this - ._checkNodeUp() - .then((isNodeUp) => { - // Try again in a few... - if (!isNodeUp) { - this._isConnecting = false; - const timeout = this.transport.retryTimeout; - - window.setTimeout(() => { - this._followConnection(); - }, timeout); - - return; - } - - this._tryNextToken(); - return false; - }); - }); - } - - connectSuccess () { - this._isConnecting = false; - this._needsToken = false; - - this.saveToken(); - - Promise - .all([ - this.parity.dappsPort(), - this.parity.dappsInterface(), - this.parity.signerPort() - ]) - .then(([dappsPort, dappsInterface, signerPort]) => { - this._dappsPort = dappsPort.toNumber(); - this._dappsInterface = dappsInterface; - this._signerPort = signerPort.toNumber(); - }); - - // DEBUG: console.log('SecureApi:connectSuccess', this._transport.token); - } - - updateToken (token) { - this._transport.updateToken(token.replace(/[^a-zA-Z0-9]/g, ''), false); - return this._followConnection(); - // DEBUG: console.log('SecureApi:updateToken', this._transport.token, connectState); + // When the transport is closed, try to reconnect + transport.on('close', this.connect, this); + this.connect(); } get dappsPort () { @@ -151,17 +52,19 @@ export default class SecureApi extends Api { } get dappsUrl () { - let hostname; + return `http://${this.hostname}:${this.dappsPort}`; + } + get hostname () { if (window.location.hostname === 'home.parity') { - hostname = 'dapps.parity'; - } else if (!this._dappsInterface || this._dappsInterface === '0.0.0.0') { - hostname = window.location.hostname; - } else { - hostname = this._dappsInterface; + return 'dapps.parity'; } - return `http://${hostname}:${this._dappsPort}`; + if (!this._dappsInterface || this._dappsInterface === '0.0.0.0') { + return window.location.hostname; + } + + return this._dappsInterface; } get signerPort () { @@ -183,4 +86,279 @@ export default class SecureApi extends Api { get secureToken () { return this._transport.token; } + + connect () { + if (this._isConnecting) { + return; + } + + log.debug('trying to connect...'); + + this._isConnecting = true; + + this.emit('connecting'); + + // Reset the tested Tokens + this._resetTokens(); + + // Try to connect + return this._connect() + .then((connected) => { + this._isConnecting = false; + + if (connected) { + const token = this.secureToken; + log.debug('got connected ; saving token', token); + + // Save the sucessful token + this._saveToken(token); + this._needsToken = false; + + // Emit the connected event + return this.emit('connected'); + } + + // If not connected, we need a new token + log.debug('needs a token'); + this._needsToken = true; + + return this.emit('disconnected'); + }) + .catch((error) => { + this._isConnecting = false; + + log.debug('emitting "disconnected"'); + this.emit('disconnected'); + console.error('unhandled error in secureApi', error); + }); + } + + /** + * Returns a Promise that gets resolved with + * a boolean: `true` if the node is up, `false` + * otherwise (HEAD request to the Node) + */ + isNodeUp () { + const url = this._url.replace(/wss?/, 'http'); + return fetch(url, { method: 'HEAD' }) + .then( + (r) => r.status === 200, + () => false + ) + .catch(() => false); + } + + /** + * Update the given token, ie. add it to the token + * list, and then try to connect (if not already connecting) + */ + updateToken (_token) { + const token = this._sanitiseToken(_token); + log.debug('updating token', token); + + // Update the tokens list: put the new one on first position + this._tokens = [ { value: token, tried: false } ].concat(this._tokens); + + // Try to connect with the new token added + return this.connect(); + } + + /** + * Try to connect to the Node with the next Token in + * the list + */ + _connect () { + log.debug('trying next token'); + + // Get the first not-tried token + const nextToken = this._getNextToken(); + + // If no more tokens to try, user has to enter a new one + if (!nextToken) { + return Promise.resolve(false); + } + + nextToken.tried = true; + + return this._connectWithToken(nextToken.value) + .then((validToken) => { + // If not valid, try again with the next token in the list + if (!validToken) { + return this._connect(); + } + + // If correct and valid token, wait until the Node is ready + // and resolve as connected + return this._waitUntilNodeReady() + .then(() => this._fetchSettings()) + .then(() => true); + }) + .catch((error) => { + log.error('unkown error in _connect', error); + return false; + }); + } + + /** + * Connect with the given token. + * It returns a Promise that gets resolved + * with `validToken` as argument, whether the given token + * is valid or not + */ + _connectWithToken (_token) { + // Sanitize the token first + const token = this._sanitiseToken(_token); + + // Update the token in the transport layer + this.transport.updateToken(token, false); + log.debug('connecting with token', token); + + return this.transport.connect() + .then(() => { + log.debug('connected with', token); + + if (token === 'initial') { + return this._generateAuthorizationToken(); + } + + // The token is valid ! + return true; + }) + .catch((error) => { + // Log if it's not a close error (ie. wrong token) + if (error && error.type !== 'close') { + log.debug('did not connect ; error', error); + } + + // Check if the Node is up + return this.isNodeUp() + .then((isNodeUp) => { + // If it's not up, try again in a few... + if (!isNodeUp) { + const timeout = this.transport.retryTimeout; + + log.debug('node is not up ; will try again in', timeout, 'ms'); + + return new Promise((resolve, reject) => { + window.setTimeout(() => { + this._connectWithToken(token).then(resolve).catch(reject); + }, timeout); + }); + } + + // The token is invalid + log.debug('tried with a wrong token', token); + return false; + }); + }); + } + + /** + * Retrieve the correct ports from the Node + */ + _fetchSettings () { + return Promise + .all([ + this.parity.dappsPort(), + this.parity.dappsInterface(), + this.parity.signerPort() + ]) + .then(([dappsPort, dappsInterface, signerPort]) => { + this._dappsPort = dappsPort.toNumber(); + this._dappsInterface = dappsInterface; + this._signerPort = signerPort.toNumber(); + }); + } + + /** + * Try to generate an Authorization Token. + * Then try to connect with the new token. + */ + _generateAuthorizationToken () { + return this.signer + .generateAuthorizationToken() + .then((token) => this._connectWithToken(token)); + } + + /** + * Get the next token to try, if any left + */ + _getNextToken () { + // Get the first not-tried token + const nextTokenIndex = this._tokens.findIndex((t) => !t.tried); + + // If no more tokens to try, user has to enter a new one + if (nextTokenIndex < 0) { + return null; + } + + const nextToken = this._tokens[nextTokenIndex]; + return nextToken; + } + + _resetTokens () { + this._tokens = this._tokens.map((token) => ({ + ...token, + tried: false + })); + } + + _sanitiseToken (token) { + return token.replace(/[^a-zA-Z0-9]/g, ''); + } + + _saveToken (token) { + window.localStorage.setItem('sysuiToken', token); + } + + /** + * Promise gets resolved when the node is up + * and running (it might take some time before + * the node is actually ready even when the client + * is connected). + * + * We check that the `parity_enode` RPC calls + * returns successfully + */ + _waitUntilNodeReady (_timeleft) { + // Default timeout to 30 seconds + const timeleft = Number.isFinite(_timeleft) + ? _timeleft + : 30 * 1000; + + // After timeout, just resolve the promise... + if (timeleft <= 0) { + console.warn('node is still not ready after 30 seconds...'); + return Promise.resolve(true); + } + + const start = Date.now(); + + return this + .parity.enode() + .then(() => true) + .catch((error) => { + if (!error) { + return true; + } + + if (error.type !== 'NETWORK_DISABLED') { + throw error; + } + + // Timeout between 250ms and 750ms + const timeout = Math.floor(250 + (500 * Math.random())); + + log.debug('waiting until node is ready', 'retry in', timeout, 'ms'); + + // Retry in a few... + return new Promise((resolve, reject) => { + window.setTimeout(() => { + const duration = Date.now() - start; + + this._waitUntilNodeReady(timeleft - duration).then(resolve).catch(reject); + }, timeout); + }); + }); + } } diff --git a/js/src/ui/Balance/balance.js b/js/src/ui/Balance/balance.js index e4dced882..b8f98c2ac 100644 --- a/js/src/ui/Balance/balance.js +++ b/js/src/ui/Balance/balance.js @@ -17,7 +17,6 @@ import BigNumber from 'bignumber.js'; import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; import unknownImage from '../../../assets/images/contracts/unknown-64x64.png'; import styles from './balance.css'; @@ -107,11 +106,7 @@ function mapStateToProps (state) { return { images }; } -function mapDispatchToProps (dispatch) { - return bindActionCreators({}, dispatch); -} - export default connect( mapStateToProps, - mapDispatchToProps + null )(Balance); diff --git a/js/src/ui/BlockStatus/blockStatus.js b/js/src/ui/BlockStatus/blockStatus.js index 47ee1a1c8..22c0fab2e 100644 --- a/js/src/ui/BlockStatus/blockStatus.js +++ b/js/src/ui/BlockStatus/blockStatus.js @@ -17,7 +17,6 @@ import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; import styles from './blockStatus.css'; @@ -113,11 +112,7 @@ function mapStateToProps (state) { }; } -function mapDispatchToProps (dispatch) { - return bindActionCreators({}, dispatch); -} - export default connect( mapStateToProps, - mapDispatchToProps + null )(BlockStatus); diff --git a/js/src/ui/Form/InputAddress/inputAddress.js b/js/src/ui/Form/InputAddress/inputAddress.js index c10ee126e..3cdac2a2e 100644 --- a/js/src/ui/Form/InputAddress/inputAddress.js +++ b/js/src/ui/Form/InputAddress/inputAddress.js @@ -16,7 +16,6 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; import util from '~/api/util'; import { nodeOrStringProptype } from '~/util/proptypes'; @@ -58,7 +57,7 @@ class InputAddress extends Component { render () { const { accountsInfo, allowCopy, className, disabled, error, focused, hint } = this.props; - const { hideUnderline, label, onClick, onFocus, onSubmit, readOnly, small } = this.props; + const { hideUnderline, label, onClick, onFocus, readOnly, small } = this.props; const { tabIndex, text, tokens, value } = this.props; const account = value && (accountsInfo[value] || tokens[value]); @@ -91,10 +90,10 @@ class InputAddress extends Component { hideUnderline={ hideUnderline } hint={ hint } label={ label } - onChange={ this.handleInputChange } + onChange={ this.onChange } onClick={ onClick } onFocus={ onFocus } - onSubmit={ onSubmit } + onSubmit={ this.onSubmit } readOnly={ readOnly } tabIndex={ tabIndex } value={ @@ -133,22 +132,34 @@ class InputAddress extends Component { return (
+ address={ value } + center + inline />
); } - handleInputChange = (event, value) => { - const isEmpty = (value.length === 0); + onChange = (event, _value) => { + let address = _value.trim(); + const isEmpty = (address.length === 0); this.setState({ isEmpty }); - if (!/^0x/.test(value) && util.isAddressValid(`0x${value}`)) { - return this.props.onChange(event, `0x${value}`); - } + if (this.props.onChange) { + if (!/^0x/.test(address) && util.isAddressValid(`0x${address}`)) { + address = `0x${address}`; + } - this.props.onChange(event, value); + this.props.onChange(event, address); + } + } + + onSubmit = (_value) => { + const address = _value.trim(); + + if (this.props.onSubmit) { + this.props.onSubmit(address); + } } } @@ -162,11 +173,7 @@ function mapStateToProps (state) { }; } -function mapDispatchToProps (dispatch) { - return bindActionCreators({}, dispatch); -} - export default connect( mapStateToProps, - mapDispatchToProps + null )(InputAddress); diff --git a/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js b/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js index f5b218694..46e4ac3b1 100644 --- a/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js +++ b/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js @@ -16,7 +16,6 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; import AddressSelect from '../AddressSelect'; @@ -68,11 +67,7 @@ function mapStateToProps (state) { }; } -function mapDispatchToProps (dispatch) { - return bindActionCreators({}, dispatch); -} - export default connect( mapStateToProps, - mapDispatchToProps + null )(InputAddressSelect); diff --git a/js/src/ui/GasPriceEditor/store.js b/js/src/ui/GasPriceEditor/store.js index 22867fdd0..bb8373854 100644 --- a/js/src/ui/GasPriceEditor/store.js +++ b/js/src/ui/GasPriceEditor/store.js @@ -62,6 +62,10 @@ export default class GasPriceEditor { this.errorTotal = errorTotal; } + @action setEstimatedError = (errorEstimated = ERRORS.gasException) => { + this.errorEstimated = errorEstimated; + } + @action setEstimated = (estimated) => { transaction(() => { const bn = new BigNumber(estimated); @@ -69,11 +73,11 @@ export default class GasPriceEditor { this.estimated = estimated; if (bn.gte(MAX_GAS_ESTIMATION)) { - this.errorEstimated = ERRORS.gasException; + this.setEstimatedError(ERRORS.gasException); } else if (bn.gte(this.gasLimit)) { - this.errorEstimated = ERRORS.gasBlockLimit; + this.setEstimatedError(ERRORS.gasBlockLimit); } else { - this.errorEstimated = null; + this.setEstimatedError(null); } }); } diff --git a/js/src/ui/GasPriceEditor/store.spec.js b/js/src/ui/GasPriceEditor/store.spec.js index 889eee4db..3dcf2e22c 100644 --- a/js/src/ui/GasPriceEditor/store.spec.js +++ b/js/src/ui/GasPriceEditor/store.spec.js @@ -82,6 +82,24 @@ describe('ui/GasPriceEditor/store', () => { }); }); + describe('setEstimatedError', () => { + it('sets the value as provided', () => { + store.setEstimatedError('errorTest'); + expect(store.errorEstimated).to.equal('errorTest'); + }); + + it('sets the null value as provided', () => { + store.setEstimatedError('errorTest'); + store.setEstimatedError(null); + expect(store.errorEstimated).to.be.null; + }); + + it('sets a default error when none provided', () => { + store.setEstimatedError(); + expect(store.errorEstimated).to.equal(ERRORS.gasException); + }); + }); + describe('setEstimated', () => { it('sets the value', () => { store.setEstimated('789'); diff --git a/js/src/ui/MethodDecoding/methodDecoding.js b/js/src/ui/MethodDecoding/methodDecoding.js index 693ae60b5..2c4c28e47 100644 --- a/js/src/ui/MethodDecoding/methodDecoding.js +++ b/js/src/ui/MethodDecoding/methodDecoding.js @@ -377,11 +377,15 @@ class MethodDecoding extends Component { } const inputs = methodInputs.map((input, index) => { + const label = input.name + ? `${input.name}: ${input.type}` + : input.type; + return ( { - const type = abi.inputs[index].type; - return { type, value }; + const { name, type } = abi.inputs[index]; + return { name, type, value }; }); } diff --git a/js/src/ui/Modal/modal.js b/js/src/ui/Modal/modal.js index d9d6ddbaf..72a9d74ae 100644 --- a/js/src/ui/Modal/modal.js +++ b/js/src/ui/Modal/modal.js @@ -16,9 +16,8 @@ import { Dialog } from 'material-ui'; import React, { Component, PropTypes } from 'react'; -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; import ReactDOM from 'react-dom'; +import { connect } from 'react-redux'; import { nodeOrStringProptype } from '~/util/proptypes'; @@ -113,11 +112,7 @@ function mapStateToProps (state) { return { settings }; } -function mapDispatchToProps (dispatch) { - return bindActionCreators({}, dispatch); -} - export default connect( mapStateToProps, - mapDispatchToProps + null )(Modal); diff --git a/js/src/ui/SignerIcon/signerIcon.js b/js/src/ui/SignerIcon/signerIcon.js index ca1ca43fa..3f6e6dd0e 100644 --- a/js/src/ui/SignerIcon/signerIcon.js +++ b/js/src/ui/SignerIcon/signerIcon.js @@ -16,7 +16,6 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; import { keccak_256 } from 'js-sha3'; // eslint-disable-line camelcase import ActionFingerprint from 'material-ui/svg-icons/action/fingerprint'; @@ -54,11 +53,7 @@ function mapStateToProps (state) { return { secureToken }; } -function mapDispatchToProps (dispatch) { - return bindActionCreators({}, dispatch); -} - export default connect( mapStateToProps, - mapDispatchToProps + null )(SignerIcon); diff --git a/js/src/ui/TxHash/txHash.js b/js/src/ui/TxHash/txHash.js index df5e9342e..c054dc01f 100644 --- a/js/src/ui/TxHash/txHash.js +++ b/js/src/ui/TxHash/txHash.js @@ -15,11 +15,10 @@ // along with Parity. If not, see . import BigNumber from 'bignumber.js'; +import { LinearProgress } from 'material-ui'; import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { LinearProgress } from 'material-ui'; import { txLink } from '~/3rdparty/etherscan/links'; import ShortenedHash from '../ShortenedHash'; @@ -169,11 +168,7 @@ function mapStateToProps (state) { return { isTest }; } -function mapDispatchToProps (dispatch) { - return bindActionCreators({}, dispatch); -} - export default connect( mapStateToProps, - mapDispatchToProps + null )(TxHash); diff --git a/js/src/ui/TxList/txList.js b/js/src/ui/TxList/txList.js index b99335a4f..5cc5618d6 100644 --- a/js/src/ui/TxList/txList.js +++ b/js/src/ui/TxList/txList.js @@ -14,10 +14,9 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { observer } from 'mobx-react'; import Store from './store'; import TxRow from './TxRow'; @@ -92,11 +91,7 @@ function mapStateToProps (state) { }; } -function mapDispatchToProps (dispatch) { - return bindActionCreators({}, dispatch); -} - export default connect( mapStateToProps, - mapDispatchToProps + null )(TxList); diff --git a/js/src/util/notifications.js b/js/src/util/notifications.js index 488ac2daf..b8bbfe08a 100644 --- a/js/src/util/notifications.js +++ b/js/src/util/notifications.js @@ -16,7 +16,6 @@ import Push from 'push.js'; import BigNumber from 'bignumber.js'; -import { noop } from 'lodash'; import { fromWei } from '~/api/util/wei'; @@ -33,13 +32,34 @@ export function notifyTransaction (account, token, _value, onClick) { ? ethereumIcon : (token.image || unkownIcon); - Push.create(`${name}`, { - body: `You just received ${value.toFormat()} ${token.tag.toUpperCase()}`, - icon: { - x16: icon, - x32: icon - }, - timeout: 20000, - onClick: onClick || noop - }); + let _notification = null; + + Push + .create(`${name}`, { + body: `You just received ${value.toFormat(3)} ${token.tag.toUpperCase()}`, + icon: { + x16: icon, + x32: icon + }, + timeout: 20000, + onClick: () => { + // Focus on the UI + try { + window.focus(); + } catch (e) {} + + if (onClick && typeof onClick === 'function') { + onClick(); + } + + // Close the notification + if (_notification) { + _notification.close(); + _notification = null; + } + } + }) + .then((notification) => { + _notification = notification; + }); } diff --git a/js/src/util/subscribe-to-events.spec.js b/js/src/util/subscribe-to-events.spec.js new file mode 100644 index 000000000..bbb0d280d --- /dev/null +++ b/js/src/util/subscribe-to-events.spec.js @@ -0,0 +1,115 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { spy, stub } from 'sinon'; + +import subscribeToEvents from './subscribe-to-events'; +import { + pastLogs, liveLogs, createApi, createContract +} from './subscribe-to-events.test.js'; + +const delay = (t) => new Promise((resolve) => { + setTimeout(resolve, t); +}); + +describe('util/subscribe-to-events', () => { + beforeEach(function () { + this.api = createApi(); + this.contract = createContract(this.api); + }); + + it('installs a filter', async function () { + const { api, contract } = this; + + subscribeToEvents(contract, [ 'Foo', 'Bar' ]); + await delay(0); + + expect(api.eth.newFilter.calledOnce).to.equal(true); + expect(api.eth.newFilter.firstCall.args).to.eql([ { + fromBlock: 0, toBlock: 'latest', + address: contract.address, + topics: [ [ + contract.instance.Foo.signature, + contract.instance.Bar.signature + ] ] + } ]); + }); + + it('queries & parses logs in the beginning', async function () { + const { api, contract } = this; + + subscribeToEvents(contract, [ 'Foo', 'Bar' ]); + + await delay(0); + expect(api.eth.getFilterLogs.callCount).to.equal(1); + expect(api.eth.getFilterLogs.firstCall.args).to.eql([ 123 ]); + + await delay(0); + expect(contract.parseEventLogs.callCount).to.equal(1); + }); + + it('emits logs in the beginning', async function () { + const { contract } = this; + + const onLog = spy(); + const onFoo = spy(); + const onBar = spy(); + subscribeToEvents(contract, [ 'Foo', 'Bar' ]) + .on('log', onLog) + .on('Foo', onFoo) + .on('Bar', onBar); + + await delay(0); + + expect(onLog.callCount).to.equal(2); + expect(onLog.firstCall.args).to.eql([ pastLogs[0] ]); + expect(onLog.secondCall.args).to.eql([ pastLogs[1] ]); + expect(onFoo.callCount).to.equal(1); + expect(onFoo.firstCall.args).to.eql([ pastLogs[0] ]); + expect(onBar.callCount).to.equal(1); + expect(onBar.firstCall.args).to.eql([ pastLogs[1] ]); + }); + + it('uninstalls the filter on sunsubscribe', async function () { + const { api, contract } = this; + + const s = subscribeToEvents(contract, [ 'Foo', 'Bar' ]); + await delay(0); + s.unsubscribe(); + await delay(0); + + expect(api.eth.uninstallFilter.calledOnce).to.equal(true); + expect(api.eth.uninstallFilter.firstCall.args).to.eql([ 123 ]); + }); + + it.skip('checks for new events regularly', async function () { + const { api, contract } = this; + api.eth.getFilterLogs = stub().resolves([]); + + const onLog = spy(); + const onBar = spy(); + const s = subscribeToEvents(contract, [ 'Bar' ], { interval: 5 }) + .on('log', onLog) + .on('Bar', onBar); + await delay(9); + s.unsubscribe(); + + expect(onLog.callCount).to.equal(1); + expect(onLog.firstCall.args).to.eql([ liveLogs[0] ]); + expect(onBar.callCount).to.equal(1); + expect(onBar.firstCall.args).to.eql([ liveLogs[0] ]); + }); +}); diff --git a/js/src/util/subscribe-to-events.test.js b/js/src/util/subscribe-to-events.test.js new file mode 100644 index 000000000..642f8e592 --- /dev/null +++ b/js/src/util/subscribe-to-events.test.js @@ -0,0 +1,53 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { stub } from 'sinon'; + +export const ADDRESS = '0x1111111111111111111111111111111111111111'; + +export const pastLogs = [ + { event: 'Foo', type: 'mined', address: ADDRESS, params: {} }, + { event: 'Bar', type: 'mined', address: ADDRESS, params: {} } +]; + +export const liveLogs = [ + { event: 'Bar', type: 'mined', address: ADDRESS, params: { foo: 'bar' } } +]; + +export const createApi = () => ({ + eth: { + newFilter: stub().resolves(123), + uninstallFilter: stub() + .rejects(new Error('unknown filter id')) + .withArgs(123).resolves(null), + getFilterLogs: stub() + .rejects(new Error('unknown filter id')) + .withArgs(123).resolves(pastLogs), + getFilterChanges: stub() + .rejects(new Error('unknown filter id')) + .withArgs(123).resolves(liveLogs) + } +}); + +export const createContract = (api) => ({ + api, + address: ADDRESS, + instance: { + Foo: { signature: 'Foo signature' }, + Bar: { signature: 'Bar signature' } + }, + parseEventLogs: stub().returnsArg(0) +}); diff --git a/js/src/views/Account/Transactions/transactions.js b/js/src/views/Account/Transactions/transactions.js index 5e48d5c5c..547c918d8 100644 --- a/js/src/views/Account/Transactions/transactions.js +++ b/js/src/views/Account/Transactions/transactions.js @@ -18,7 +18,6 @@ import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; import { Container, TxList, Loading } from '~/ui'; @@ -118,11 +117,7 @@ function mapStateToProps (state) { }; } -function mapDispatchToProps (dispatch) { - return bindActionCreators({}, dispatch); -} - export default connect( mapStateToProps, - mapDispatchToProps + null )(Transactions); diff --git a/js/src/views/Application/Status/status.js b/js/src/views/Application/Status/status.js index 1aded4b88..54dad960e 100644 --- a/js/src/views/Application/Status/status.js +++ b/js/src/views/Application/Status/status.js @@ -17,7 +17,6 @@ import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; import { BlockStatus } from '~/ui'; @@ -60,6 +59,10 @@ class Status extends Component { renderConsensus () { const { upgradeStore } = this.props; + if (!upgradeStore || !upgradeStore.consensusCapability) { + return null; + } + if (upgradeStore.consensusCapability === 'capable') { return (
@@ -68,7 +71,9 @@ class Status extends Component { defaultMessage='Capable' />
); - } else if (upgradeStore.consensusCapability.capableUntil) { + } + + if (upgradeStore.consensusCapability.capableUntil) { return (
); - } else if (upgradeStore.consensusCapability.incapableSince) { + } + + if (upgradeStore.consensusCapability.incapableSince) { return (
. +import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { observer } from 'mobx-react'; import UpgradeStore from '~/modals/UpgradeParity/store'; @@ -123,11 +122,7 @@ function mapStateToProps (state) { }; } -function mapDispatchToProps (dispatch) { - return bindActionCreators({}, dispatch); -} - export default connect( mapStateToProps, - mapDispatchToProps + null )(Application); diff --git a/js/src/views/Connection/connection.js b/js/src/views/Connection/connection.js index 505840e1e..81b6d536c 100644 --- a/js/src/views/Connection/connection.js +++ b/js/src/views/Connection/connection.js @@ -41,9 +41,9 @@ class Connection extends Component { } render () { - const { isConnected, needsToken } = this.props; + const { isConnecting, isConnected, needsToken } = this.props; - if (isConnected) { + if (!isConnecting && isConnected) { return null; } diff --git a/js/src/views/Dapps/dapps.js b/js/src/views/Dapps/dapps.js index 8ebbb602d..27e5c8b52 100644 --- a/js/src/views/Dapps/dapps.js +++ b/js/src/views/Dapps/dapps.js @@ -14,13 +14,12 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +import { omitBy } from 'lodash'; import { Checkbox } from 'material-ui'; import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { omitBy } from 'lodash'; import { AddDapps, DappPermissions } from '~/modals'; import PermissionStore from '~/modals/DappPermissions/store'; @@ -169,11 +168,7 @@ function mapStateToProps (state) { }; } -function mapDispatchToProps (dispatch) { - return bindActionCreators({}, dispatch); -} - export default connect( mapStateToProps, - mapDispatchToProps + null )(Dapps); diff --git a/js/src/views/Dapps/dappsStore.js b/js/src/views/Dapps/dappsStore.js index 8cca4d3f7..efbde9ef4 100644 --- a/js/src/views/Dapps/dappsStore.js +++ b/js/src/views/Dapps/dappsStore.js @@ -256,7 +256,7 @@ export default class DappsStore { store.set(LS_KEY_DISPLAY, this.displayApps); } - @action addApps = (_apps) => { + @action addApps = (_apps = []) => { transaction(() => { const apps = _apps.filter((app) => app); diff --git a/js/src/views/ParityBar/parityBar.js b/js/src/views/ParityBar/parityBar.js index 42c52f47d..8893ac566 100644 --- a/js/src/views/ParityBar/parityBar.js +++ b/js/src/views/ParityBar/parityBar.js @@ -17,7 +17,6 @@ import React, { Component, PropTypes } from 'react'; import { Link } from 'react-router'; import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; import ActionFingerprint from 'material-ui/svg-icons/action/fingerprint'; import ContentClear from 'material-ui/svg-icons/content/clear'; @@ -162,11 +161,7 @@ function mapStateToProps (state) { }; } -function mapDispatchToProps (dispatch) { - return bindActionCreators({}, dispatch); -} - export default connect( mapStateToProps, - mapDispatchToProps + null )(ParityBar); diff --git a/js/src/views/Settings/Parity/parity.js b/js/src/views/Settings/Parity/parity.js index c52d713cd..d207c04dd 100644 --- a/js/src/views/Settings/Parity/parity.js +++ b/js/src/views/Settings/Parity/parity.js @@ -51,7 +51,7 @@ export default class Parity extends Component { Object.keys(LOG_KEYS).map((logKey) => { const log = LOG_KEYS[logKey]; - const logger = LogLevel.getLogger(log.path); + const logger = LogLevel.getLogger(log.key); const level = logger.getLevel(); nextState[logKey] = { level, log }; @@ -133,11 +133,11 @@ export default class Parity extends Component { return Object.keys(logLevels).map((logKey) => { const { level, log } = logLevels[logKey]; - const { path, desc } = log; + const { key, desc } = log; const onChange = (_, index) => { const nextLevel = Object.values(selectValues)[index].value; - LogLevel.getLogger(path).setLevel(nextLevel); + LogLevel.getLogger(key).setLevel(nextLevel); this.loadLogLevels(); }; diff --git a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js index 5a93cef0d..f9d93cbe1 100644 --- a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js +++ b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js @@ -14,13 +14,12 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +import keycode from 'keycode'; +import RaisedButton from 'material-ui/RaisedButton'; import React, { Component, PropTypes } from 'react'; import ReactDOM from 'react-dom'; import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import RaisedButton from 'material-ui/RaisedButton'; import ReactTooltip from 'react-tooltip'; -import keycode from 'keycode'; import { Form, Input, IdentityIcon } from '~/ui'; @@ -258,11 +257,7 @@ function mapStateToProps (_, initProps) { }; } -function mapDispatchToProps (dispatch) { - return bindActionCreators({}, dispatch); -} - export default connect( mapStateToProps, - mapDispatchToProps + null )(TransactionPendingFormConfirm); diff --git a/js/src/views/Status/components/MiningSettings/miningSettings.js b/js/src/views/Status/components/MiningSettings/miningSettings.js index b65eed929..8cc04d45e 100644 --- a/js/src/views/Status/components/MiningSettings/miningSettings.js +++ b/js/src/views/Status/components/MiningSettings/miningSettings.js @@ -37,6 +37,14 @@ export default class MiningSettings extends Component { const { nodeStatus } = this.props; const { coinbase, defaultExtraData, extraData, gasFloorTarget, minGasPrice } = nodeStatus; + const extradata = extraData + ? decodeExtraData(extraData) + : ''; + + const defaultExtradata = defaultExtraData + ? decodeExtraData(defaultExtraData) + : ''; + return (
@@ -53,9 +61,9 @@ export default class MiningSettings extends Component { @@ -121,7 +127,7 @@ export default class Status extends Component { allowCopy readOnly label='network port' - value={ nodeStatus.netPort.toString() } + value={ netPort.toString() } { ...this._test('network-port') } />
@@ -146,7 +152,7 @@ export default class Status extends Component { allowCopy readOnly label='rpc port' - value={ rpcSettings.port.toString() } + value={ rpcPort.toString() } { ...this._test('rpc-port') } /> diff --git a/js/test/mockRpc.js b/js/test/mockRpc.js index 13f752f8e..fef27de4b 100644 --- a/js/test/mockRpc.js +++ b/js/test/mockRpc.js @@ -47,8 +47,8 @@ export function mockHttp (requests) { } export function mockWs (requests) { - const scope = { requests: 0, body: {} }; let mockServer = new MockWsServer(TEST_WS_URL); + const scope = { requests: 0, body: {}, server: mockServer }; scope.isDone = () => scope.requests === requests.length; scope.stop = () => { diff --git a/parity/configuration.rs b/parity/configuration.rs index 671c78206..42816a823 100644 --- a/parity/configuration.rs +++ b/parity/configuration.rs @@ -20,9 +20,10 @@ use std::net::SocketAddr; use std::path::{Path, PathBuf}; use std::cmp::max; use cli::{Args, ArgsError}; -use util::{Hashable, U256, Uint, Bytes, version_data, Secret, Address}; +use util::{Hashable, U256, Uint, Bytes, version_data, Address}; use util::log::Colour; use ethsync::{NetworkConfiguration, is_valid_node_url, AllowIP}; +use ethcore::ethstore::ethkey::Secret; use ethcore::client::{VMType}; use ethcore::miner::{MinerOptions, Banning}; use ethcore::verification::queue::VerifierSettings; @@ -603,7 +604,13 @@ impl Configuration { let (listen, public) = self.net_addresses()?; ret.listen_address = listen.map(|l| format!("{}", l)); ret.public_address = public.map(|p| format!("{}", p)); - ret.use_secret = self.args.flag_node_key.as_ref().map(|s| s.parse::().unwrap_or_else(|_| s.sha3())); + ret.use_secret = match self.args.flag_node_key.as_ref() + .map(|s| s.parse::().or_else(|_| Secret::from_slice(&s.sha3())).map_err(|e| format!("Invalid key: {:?}", e)) + ) { + None => None, + Some(Ok(key)) => Some(key), + Some(Err(err)) => return Err(err), + }; ret.discovery_enabled = !self.args.flag_no_discovery && !self.args.flag_nodiscover; ret.max_peers = self.max_peers(); ret.min_peers = self.min_peers(); diff --git a/parity/presale.rs b/parity/presale.rs index 02ae8dfdd..d5d028367 100644 --- a/parity/presale.rs +++ b/parity/presale.rs @@ -40,6 +40,6 @@ pub fn execute(cmd: ImportWallet) -> Result { let acc_provider = AccountProvider::new(secret_store); let wallet = PresaleWallet::open(cmd.wallet_path).map_err(|_| "Unable to open presale wallet.")?; let kp = wallet.decrypt(&password).map_err(|_| "Invalid password.")?; - let address = acc_provider.insert_account(*kp.secret(), &password).unwrap(); + let address = acc_provider.insert_account(kp.secret().clone(), &password).unwrap(); Ok(format!("{:?}", address)) } diff --git a/rpc/src/v1/helpers/errors.rs b/rpc/src/v1/helpers/errors.rs index 80a0dab9a..00f383a04 100644 --- a/rpc/src/v1/helpers/errors.rs +++ b/rpc/src/v1/helpers/errors.rs @@ -36,6 +36,7 @@ mod codes { pub const UNKNOWN_ERROR: i64 = -32009; pub const TRANSACTION_ERROR: i64 = -32010; pub const EXECUTION_ERROR: i64 = -32015; + pub const EXCEPTION_ERROR: i64 = -32016; pub const ACCOUNT_LOCKED: i64 = -32020; pub const PASSWORD_INVALID: i64 = -32021; pub const ACCOUNT_ERROR: i64 = -32023; @@ -130,6 +131,14 @@ pub fn state_pruned() -> Error { } } +pub fn exceptional() -> Error { + Error { + code: ErrorCode::ServerError(codes::EXCEPTION_ERROR), + message: "The execution failed due to an exception.".into(), + data: None + } +} + pub fn no_work() -> Error { Error { code: ErrorCode::ServerError(codes::NO_WORK), @@ -286,6 +295,7 @@ pub fn from_rlp_error(error: DecoderError) -> Error { pub fn from_call_error(error: CallError) -> Error { match error { CallError::StatePruned => state_pruned(), + CallError::Exceptional => exceptional(), CallError::Execution(e) => execution(e), CallError::TransactionNotFound => internal("{}, this should not be the case with eth_call, most likely a bug.", CallError::TransactionNotFound), } diff --git a/rpc/src/v1/impls/eth.rs b/rpc/src/v1/impls/eth.rs index fd5f3d4f3..75817838d 100644 --- a/rpc/src/v1/impls/eth.rs +++ b/rpc/src/v1/impls/eth.rs @@ -660,7 +660,6 @@ impl Eth for EthClient where num => take_weak!(self.client).call(&signed, num.into(), Default::default()), }; - result .map(|b| b.output.into()) .map_err(errors::from_call_error) diff --git a/rpc/src/v1/impls/parity_accounts.rs b/rpc/src/v1/impls/parity_accounts.rs index fa1f776b5..5970b1ec7 100644 --- a/rpc/src/v1/impls/parity_accounts.rs +++ b/rpc/src/v1/impls/parity_accounts.rs @@ -19,7 +19,7 @@ use std::sync::{Arc, Weak}; use std::collections::BTreeMap; use util::{Address}; -use ethkey::{Brain, Generator}; +use ethkey::{Brain, Generator, Secret}; use ethcore::account_provider::AccountProvider; use ethcore::client::MiningBlockChainClient; @@ -73,7 +73,8 @@ impl ParityAccounts for ParityAccountsClient where C: MiningBlock self.active()?; let store = take_weak!(self.accounts); - store.insert_account(*Brain::new(phrase).generate().unwrap().secret(), &pass) + let brain = Brain::new(phrase).generate().unwrap(); + store.insert_account(brain.secret().clone(), &pass) .map(Into::into) .map_err(|e| errors::account("Could not create account.", e)) } @@ -92,7 +93,9 @@ impl ParityAccounts for ParityAccountsClient where C: MiningBlock self.active()?; let store = take_weak!(self.accounts); - store.insert_account(secret.into(), &pass) + let secret = Secret::from_slice(&secret.0) + .map_err(|e| errors::account("Could not create account.", e))?; + store.insert_account(secret, &pass) .map(Into::into) .map_err(|e| errors::account("Could not create account.", e)) } diff --git a/rpc/src/v1/tests/eth.rs b/rpc/src/v1/tests/eth.rs index 428cae3e0..27e25b64e 100644 --- a/rpc/src/v1/tests/eth.rs +++ b/rpc/src/v1/tests/eth.rs @@ -307,7 +307,7 @@ const POSITIVE_NONCE_SPEC: &'static [u8] = br#"{ #[test] fn eth_transaction_count() { - let secret = "8a283037bb19c4fed7b1c569e40c7dcff366165eb869110a1b11532963eb9cb2".into(); + let secret = "8a283037bb19c4fed7b1c569e40c7dcff366165eb869110a1b11532963eb9cb2".parse().unwrap(); let tester = EthTester::from_spec(Spec::load(TRANSACTION_COUNT_SPEC).expect("invalid chain spec")); let address = tester.accounts.insert_account(secret, "").unwrap(); tester.accounts.unlock_account_permanently(address, "".into()).unwrap(); diff --git a/rpc/src/v1/tests/mocked/eth.rs b/rpc/src/v1/tests/mocked/eth.rs index 5f4424313..3b5ce50b3 100644 --- a/rpc/src/v1/tests/mocked/eth.rs +++ b/rpc/src/v1/tests/mocked/eth.rs @@ -561,6 +561,7 @@ fn rpc_eth_code() { fn rpc_eth_call_latest() { let tester = EthTester::default(); tester.client.set_execution_result(Ok(Executed { + exception: None, gas: U256::zero(), gas_used: U256::from(0xff30), refunded: U256::from(0x5), @@ -596,6 +597,7 @@ fn rpc_eth_call_latest() { fn rpc_eth_call() { let tester = EthTester::default(); tester.client.set_execution_result(Ok(Executed { + exception: None, gas: U256::zero(), gas_used: U256::from(0xff30), refunded: U256::from(0x5), @@ -631,6 +633,7 @@ fn rpc_eth_call() { fn rpc_eth_call_default_block() { let tester = EthTester::default(); tester.client.set_execution_result(Ok(Executed { + exception: None, gas: U256::zero(), gas_used: U256::from(0xff30), refunded: U256::from(0x5), @@ -665,6 +668,7 @@ fn rpc_eth_call_default_block() { fn rpc_eth_estimate_gas() { let tester = EthTester::default(); tester.client.set_execution_result(Ok(Executed { + exception: None, gas: U256::zero(), gas_used: U256::from(0xff30), refunded: U256::from(0x5), @@ -700,6 +704,7 @@ fn rpc_eth_estimate_gas() { fn rpc_eth_estimate_gas_default_block() { let tester = EthTester::default(); tester.client.set_execution_result(Ok(Executed { + exception: None, gas: U256::zero(), gas_used: U256::from(0xff30), refunded: U256::from(0x5), diff --git a/rpc/src/v1/tests/mocked/traces.rs b/rpc/src/v1/tests/mocked/traces.rs index f9a9baa00..ffd6b717f 100644 --- a/rpc/src/v1/tests/mocked/traces.rs +++ b/rpc/src/v1/tests/mocked/traces.rs @@ -51,6 +51,7 @@ fn io() -> Tester { block_hash: 10.into(), }]); *client.execution_result.write() = Some(Ok(Executed { + exception: None, gas: 20_000.into(), gas_used: 10_000.into(), refunded: 0.into(), diff --git a/rpc/src/v1/types/hash.rs b/rpc/src/v1/types/hash.rs index cc4532e7c..978c3a0bf 100644 --- a/rpc/src/v1/types/hash.rs +++ b/rpc/src/v1/types/hash.rs @@ -25,7 +25,7 @@ use util::{H64 as Eth64, H160 as Eth160, H256 as Eth256, H520 as Eth520, H512 as macro_rules! impl_hash { ($name: ident, $other: ident, $size: expr) => { /// Hash serialization - pub struct $name([u8; $size]); + pub struct $name(pub [u8; $size]); impl Eq for $name { } diff --git a/sync/src/api.rs b/sync/src/api.rs index 6c2c43db4..36f4a0d9a 100644 --- a/sync/src/api.rs +++ b/sync/src/api.rs @@ -23,6 +23,7 @@ use network::{NetworkProtocolHandler, NetworkService, NetworkContext, PeerId, Pr AllowIP as NetworkAllowIP}; use util::{U256, H256, H512}; use io::{TimerToken}; +use ethcore::ethstore::ethkey::Secret; use ethcore::client::{BlockChainClient, ChainNotify}; use ethcore::snapshot::SnapshotService; use ethcore::header::BlockNumber; @@ -476,7 +477,7 @@ pub struct NetworkConfiguration { /// List of initial node addresses pub boot_nodes: Vec, /// Use provided node key instead of default - pub use_secret: Option, + pub use_secret: Option, /// Max number of connected peers to maintain pub max_peers: u32, /// Min number of connected peers to maintain @@ -667,3 +668,4 @@ impl ManageNetwork for LightSync { NetworkConfiguration::from(self.network.config().clone()) } } + diff --git a/sync/src/tests/consensus.rs b/sync/src/tests/consensus.rs index ea8bd970d..82b990f46 100644 --- a/sync/src/tests/consensus.rs +++ b/sync/src/tests/consensus.rs @@ -22,7 +22,7 @@ use ethcore::spec::Spec; use ethcore::miner::MinerService; use ethcore::transaction::*; use ethcore::account_provider::AccountProvider; -use ethkey::KeyPair; +use ethkey::{KeyPair, Secret}; use super::helpers::*; use SyncConfig; @@ -41,7 +41,7 @@ impl IoHandler for TestIoHandler { } } -fn new_tx(secret: &H256, nonce: U256) -> PendingTransaction { +fn new_tx(secret: &Secret, nonce: U256) -> PendingTransaction { let signed = Transaction { nonce: nonce.into(), gas_price: 0.into(), @@ -55,8 +55,8 @@ fn new_tx(secret: &H256, nonce: U256) -> PendingTransaction { #[test] fn authority_round() { - let s0 = KeyPair::from_secret("1".sha3()).unwrap(); - let s1 = KeyPair::from_secret("0".sha3()).unwrap(); + let s0 = KeyPair::from_secret_slice(&"1".sha3()).unwrap(); + let s1 = KeyPair::from_secret_slice(&"0".sha3()).unwrap(); let spec_factory = || { let spec = Spec::new_test_round(); let account_provider = AccountProvider::transient_provider(); @@ -118,8 +118,8 @@ fn authority_round() { #[test] fn tendermint() { - let s0 = KeyPair::from_secret("1".sha3()).unwrap(); - let s1 = KeyPair::from_secret("0".sha3()).unwrap(); + let s0 = KeyPair::from_secret_slice(&"1".sha3()).unwrap(); + let s1 = KeyPair::from_secret_slice(&"0".sha3()).unwrap(); let spec_factory = || { let spec = Spec::new_test_tendermint(); let account_provider = AccountProvider::transient_provider(); diff --git a/util/network/src/handshake.rs b/util/network/src/handshake.rs index eb04ce869..3de7417d1 100644 --- a/util/network/src/handshake.rs +++ b/util/network/src/handshake.rs @@ -165,7 +165,7 @@ impl Handshake { self.id.clone_from_slice(remote_public); self.remote_nonce.clone_from_slice(remote_nonce); self.remote_version = remote_version; - let shared = ecdh::agree(host_secret, &self.id)?; + let shared = *ecdh::agree(host_secret, &self.id)?; let signature = H520::from_slice(sig); self.remote_ephemeral = recover(&signature.into(), &(&shared ^ &self.remote_nonce))?; Ok(()) @@ -271,7 +271,7 @@ impl Handshake { let (nonce, _) = rest.split_at_mut(32); // E(remote-pubk, S(ecdhe-random, ecdh-shared-secret^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x0) - let shared = ecdh::agree(secret, &self.id)?; + let shared = *ecdh::agree(secret, &self.id)?; sig.copy_from_slice(&*sign(self.ecdhe.secret(), &(&shared ^ &self.nonce))?); self.ecdhe.public().sha3_into(hepubk); pubk.copy_from_slice(public); @@ -366,7 +366,7 @@ mod test { #[test] fn test_handshake_auth_plain() { let mut h = create_handshake(None); - let secret = "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291".into(); + let secret = "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291".parse().unwrap(); let auth = "\ 048ca79ad18e4b0659fab4853fe5bc58eb83992980f4c9cc147d2aa31532efd29a3d3dc6a3d89eaf\ @@ -387,7 +387,7 @@ mod test { #[test] fn test_handshake_auth_eip8() { let mut h = create_handshake(None); - let secret = "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291".into(); + let secret = "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291".parse().unwrap(); let auth = "\ 01b304ab7578555167be8154d5cc456f567d5ba302662433674222360f08d5f1534499d3678b513b\ @@ -413,7 +413,7 @@ mod test { #[test] fn test_handshake_auth_eip8_2() { let mut h = create_handshake(None); - let secret = "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291".into(); + let secret = "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291".parse().unwrap(); let auth = "\ 01b8044c6c312173685d1edd268aa95e1d495474c6959bcdd10067ba4c9013df9e40ff45f5bfd6f7\ @@ -444,7 +444,7 @@ mod test { fn test_handshake_ack_plain() { let remote = "fda1cff674c90c9a197539fe3dfb53086ace64f83ed7c6eabec741f7f381cc803e52ab2cd55d5569bce4347107a310dfd5f88a010cd2ffd1005ca406f1842877".into(); let mut h = create_handshake(Some(&remote)); - let secret = "49a7b37aa6f6645917e7b807e9d1c00d4fa71f18343b0d4122a4d2df64dd6fee".into(); + let secret = "49a7b37aa6f6645917e7b807e9d1c00d4fa71f18343b0d4122a4d2df64dd6fee".parse().unwrap(); let ack = "\ 049f8abcfa9c0dc65b982e98af921bc0ba6e4243169348a236abe9df5f93aa69d99cadddaa387662\ @@ -464,7 +464,7 @@ mod test { fn test_handshake_ack_eip8() { let remote = "fda1cff674c90c9a197539fe3dfb53086ace64f83ed7c6eabec741f7f381cc803e52ab2cd55d5569bce4347107a310dfd5f88a010cd2ffd1005ca406f1842877".into(); let mut h = create_handshake(Some(&remote)); - let secret = "49a7b37aa6f6645917e7b807e9d1c00d4fa71f18343b0d4122a4d2df64dd6fee".into(); + let secret = "49a7b37aa6f6645917e7b807e9d1c00d4fa71f18343b0d4122a4d2df64dd6fee".parse().unwrap(); let ack = "\ 01ea0451958701280a56482929d3b0757da8f7fbe5286784beead59d95089c217c9b917788989470\ @@ -493,7 +493,7 @@ mod test { fn test_handshake_ack_eip8_2() { let remote = "fda1cff674c90c9a197539fe3dfb53086ace64f83ed7c6eabec741f7f381cc803e52ab2cd55d5569bce4347107a310dfd5f88a010cd2ffd1005ca406f1842877".into(); let mut h = create_handshake(Some(&remote)); - let secret = "49a7b37aa6f6645917e7b807e9d1c00d4fa71f18343b0d4122a4d2df64dd6fee".into(); + let secret = "49a7b37aa6f6645917e7b807e9d1c00d4fa71f18343b0d4122a4d2df64dd6fee".parse().unwrap(); let ack = "\ 01f004076e58aae772bb101ab1a8e64e01ee96e64857ce82b1113817c6cdd52c09d26f7b90981cd7\ diff --git a/util/network/src/host.rs b/util/network/src/host.rs index ba966e07c..2f236a5f7 100644 --- a/util/network/src/host.rs +++ b/util/network/src/host.rs @@ -1207,7 +1207,7 @@ fn load_key(path: &Path) -> Option { fn key_save_load() { use ::devtools::RandomTempPath; let temp_path = RandomTempPath::create_dir(); - let key = H256::random(); + let key = Secret::from_slice(&H256::random()).unwrap(); save_key(temp_path.as_path(), &key); let r = load_key(temp_path.as_path()); assert_eq!(key, r.unwrap()); @@ -1217,8 +1217,9 @@ fn key_save_load() { #[test] fn host_client_url() { let mut config = NetworkConfiguration::new_local(); - let key = "6f7b0d801bc7b5ce7bbd930b84fd0369b3eb25d09be58d64ba811091046f3aa2".into(); + let key = "6f7b0d801bc7b5ce7bbd930b84fd0369b3eb25d09be58d64ba811091046f3aa2".parse().unwrap(); config.use_secret = Some(key); let host: Host = Host::new(config, Arc::new(NetworkStats::new())).unwrap(); assert!(host.local_url().starts_with("enode://101b3ef5a4ea7a1c7928e24c4c75fd053c235d7b80c22ae5c03d145d0ac7396e2a4ffff9adee3133a7b05044a5cee08115fd65145e5165d646bde371010d803c@")); } + diff --git a/util/src/lib.rs b/util/src/lib.rs index e37214879..9b4d3be59 100644 --- a/util/src/lib.rs +++ b/util/src/lib.rs @@ -164,6 +164,3 @@ pub use timer::*; /// 160-bit integer representing account address pub type Address = H160; - -/// Secret -pub type Secret = H256;