diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index 93cb8a6f23..73ac949723 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -2360,6 +2360,7 @@ dependencies = [ "srml-executive 2.0.0", "srml-finality-tracker 2.0.0", "srml-grandpa 2.0.0", + "srml-im-online 0.1.0", "srml-indices 2.0.0", "srml-session 2.0.0", "srml-staking 2.0.0", @@ -3844,6 +3845,21 @@ dependencies = [ "substrate-primitives 2.0.0", ] +[[package]] +name = "srml-im-online" +version = "0.1.0" +dependencies = [ + "parity-codec 4.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)", + "sr-io 2.0.0", + "sr-primitives 2.0.0", + "sr-std 2.0.0", + "srml-session 2.0.0", + "srml-support 2.0.0", + "srml-system 2.0.0", + "substrate-primitives 2.0.0", +] + [[package]] name = "srml-indices" version = "2.0.0" @@ -4564,6 +4580,7 @@ dependencies = [ "sr-primitives 2.0.0", "substrate-client 2.0.0", "substrate-client-db 2.0.0", + "substrate-network 2.0.0", "substrate-offchain-primitives 2.0.0", "substrate-primitives 2.0.0", "substrate-test-runtime-client 2.0.0", @@ -4710,6 +4727,7 @@ dependencies = [ "node-primitives 2.0.0", "node-runtime 2.0.0", "parity-codec 4.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-multiaddr 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/substrate/Cargo.toml b/substrate/Cargo.toml index b8b6311a30..d2d1df56a9 100644 --- a/substrate/Cargo.toml +++ b/substrate/Cargo.toml @@ -85,6 +85,7 @@ members = [ "srml/system", "srml/timestamp", "srml/treasury", + "srml/im-online", "node/cli", "node/executor", "node/primitives", diff --git a/substrate/core/executor/src/wasm_executor.rs b/substrate/core/executor/src/wasm_executor.rs index 83f1cf5a4b..3bb8412be2 100644 --- a/substrate/core/executor/src/wasm_executor.rs +++ b/substrate/core/executor/src/wasm_executor.rs @@ -26,6 +26,7 @@ use wasmi::{ }; use state_machine::{Externalities, ChildStorageKey}; use crate::error::{Error, Result}; +use parity_codec::Encode; use primitives::{blake2_128, blake2_256, twox_64, twox_128, twox_256, ed25519, sr25519, Pair}; use primitives::offchain; use primitives::hexdisplay::HexDisplay; @@ -767,6 +768,42 @@ impl_function_executor!(this: FunctionExecutor<'e, E>, Ok(offset) }, + ext_network_state(written_out: *mut u32) -> *mut u8 => { + let res = this.ext.offchain() + .map(|api| api.network_state()) + .ok_or_else(|| "Calling unavailable API ext_network_state: wasm")?; + + let encoded = res.encode(); + let len = encoded.len() as u32; + let offset = this.heap.allocate(len)? as u32; + this.memory.set(offset, &encoded) + .map_err(|_| "Invalid attempt to set memory in ext_network_state")?; + + this.memory.write_primitive(written_out, len) + .map_err(|_| "Invalid attempt to write written_out in ext_network_state")?; + + Ok(offset) + }, + ext_authority_pubkey( + kind: u32, + written_out: *mut u32 + ) -> *mut u8 => { + let kind = offchain::CryptoKind::try_from(kind) + .map_err(|_| "crypto kind OOB while ext_authority_pubkey: wasm")?; + + let res = this.ext.offchain() + .map(|api| api.authority_pubkey(kind)) + .ok_or_else(|| "Calling unavailable API ext_authority_pubkey: wasm")?; + + let encoded = res.encode(); + let len = encoded.len() as u32; + let offset = this.heap.allocate(len)? as u32; + this.memory.set(offset, &encoded) + .map_err(|_| "Invalid attempt to set memory in ext_authority_pubkey")?; + this.memory.write_primitive(written_out, len) + .map_err(|_| "Invalid attempt to write written_out in ext_authority_pubkey")?; + Ok(offset) + }, ext_decrypt( key: u32, kind: u32, diff --git a/substrate/core/network/src/lib.rs b/substrate/core/network/src/lib.rs index 98cbf75c63..7b740976d2 100644 --- a/substrate/core/network/src/lib.rs +++ b/substrate/core/network/src/lib.rs @@ -189,6 +189,7 @@ pub mod test; pub use chain::{Client as ClientHandle, FinalityProofProvider}; pub use service::{ NetworkService, NetworkWorker, TransactionPool, ExHashT, ReportHandle, + NetworkStateInfo, }; pub use protocol::{PeerInfo, Context, consensus_gossip, message, specialization}; pub use protocol::sync::SyncState; diff --git a/substrate/core/network/src/service.rs b/substrate/core/network/src/service.rs index 5841336b44..4cbe704eae 100644 --- a/substrate/core/network/src/service.rs +++ b/substrate/core/network/src/service.rs @@ -34,6 +34,7 @@ use futures::{prelude::*, sync::mpsc}; use log::{warn, error, info}; use libp2p::core::{swarm::NetworkBehaviour, transport::boxed::Boxed, muxing::StreamMuxerBox}; use libp2p::{PeerId, Multiaddr, multihash::Multihash}; +use parking_lot::Mutex; use peerset::PeersetHandle; use runtime_primitives::{traits::{Block as BlockT, NumberFor}, ConsensusEngineId}; @@ -86,6 +87,8 @@ impl ReportHandle { pub struct NetworkService, H: ExHashT> { /// Number of peers we're connected to. num_connected: Arc, + /// The local external addresses. + external_addresses: Arc>>, /// Are we actively catching up with the chain? is_major_syncing: Arc, /// Local copy of the `PeerId` of the local node. @@ -215,8 +218,11 @@ impl, H: ExHashT> NetworkWorker Swarm::::add_external_address(&mut swarm, addr.clone()); } + let external_addresses = Arc::new(Mutex::new(Vec::new())); + let service = Arc::new(NetworkService { bandwidth, + external_addresses: external_addresses.clone(), num_connected: num_connected.clone(), is_major_syncing: is_major_syncing.clone(), peerset: peerset_handle, @@ -226,6 +232,7 @@ impl, H: ExHashT> NetworkWorker }); Ok(NetworkWorker { + external_addresses, num_connected, is_major_syncing, network_service: swarm, @@ -295,7 +302,7 @@ impl, H: ExHashT> NetworkWorker /// Get network state. /// /// **Note**: Use this only for debugging. This API is unstable. There are warnings literaly - /// everywhere about this. Please don't use this function to retreive actual information. + /// everywhere about this. Please don't use this function to retrieve actual information. pub fn network_state(&mut self) -> NetworkState { let swarm = &mut self.network_service; let open = swarm.user_protocol().open_peers().cloned().collect::>(); @@ -487,6 +494,11 @@ impl, H: ExHashT> NetworkServic pub fn num_connected(&self) -> usize { self.num_connected.load(Ordering::Relaxed) } + + /// Returns the local external addresses. + pub fn external_addresses(&self) -> Vec { + self.external_addresses.lock().clone() + } } impl, H: ExHashT> @@ -500,6 +512,32 @@ impl, H: ExHashT> } } +/// Trait for providing information about the local network state +pub trait NetworkStateInfo { + /// Returns the local external addresses. + fn external_addresses(&self) -> Vec; + + /// Returns the local Peer ID. + fn peer_id(&self) -> PeerId; +} + +impl NetworkStateInfo for NetworkService + where + B: runtime_primitives::traits::Block, + S: NetworkSpecialization, + H: ExHashT, +{ + /// Returns the local external addresses. + fn external_addresses(&self) -> Vec { + self.external_addresses.lock().clone() + } + + /// Returns the local Peer ID. + fn peer_id(&self) -> PeerId { + self.local_peer_id.clone() + } +} + /// Messages sent from the `NetworkService` to the `NetworkWorker`. /// /// Each entry corresponds to a method of `NetworkService`. @@ -520,6 +558,8 @@ enum ServerToWorkerMsg> { /// You are encouraged to poll this in a separate background thread or task. #[must_use = "The NetworkWorker must be polled in order for the network to work"] pub struct NetworkWorker, H: ExHashT> { + /// Updated by the `NetworkWorker` and loaded by the `NetworkService`. + external_addresses: Arc>>, /// Updated by the `NetworkWorker` and loaded by the `NetworkService`. num_connected: Arc, /// Updated by the `NetworkWorker` and loaded by the `NetworkService`. @@ -621,6 +661,10 @@ impl, H: ExHashT> Future for Ne // Update the variables shared with the `NetworkService`. self.num_connected.store(self.network_service.user_protocol_mut().num_connected_peers(), Ordering::Relaxed); + { + let external_addresses = Swarm::::external_addresses(&self.network_service).cloned().collect(); + *self.external_addresses.lock() = external_addresses; + } self.is_major_syncing.store(match self.network_service.user_protocol_mut().sync_state() { SyncState::Idle => false, SyncState::Downloading => true, diff --git a/substrate/core/offchain/Cargo.toml b/substrate/core/offchain/Cargo.toml index 758865b49c..ecc098ae08 100644 --- a/substrate/core/offchain/Cargo.toml +++ b/substrate/core/offchain/Cargo.toml @@ -16,6 +16,7 @@ parking_lot = "0.8.0" primitives = { package = "substrate-primitives", path = "../../core/primitives" } runtime_primitives = { package = "sr-primitives", path = "../../core/sr-primitives" } transaction_pool = { package = "substrate-transaction-pool", path = "../../core/transaction-pool" } +network = { package = "substrate-network", path = "../../core/network" } [dev-dependencies] env_logger = "0.6" diff --git a/substrate/core/offchain/src/api.rs b/substrate/core/offchain/src/api.rs index b6aba784b3..776b31ef12 100644 --- a/substrate/core/offchain/src/api.rs +++ b/substrate/core/offchain/src/api.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Substrate. If not, see . -use std::sync::Arc; +use std::{str::FromStr, sync::Arc, convert::TryFrom}; use client::backend::OffchainStorage; use crate::AuthorityKeyProvider; use futures::{Stream, Future, sync::mpsc}; @@ -25,6 +25,7 @@ use primitives::offchain::{ Externalities as OffchainExt, CryptoKind, CryptoKeyId, StorageKind, + OpaqueNetworkState, OpaquePeerId, OpaqueMultiaddr, }; use primitives::crypto::{Pair, Protected}; use primitives::{ed25519, sr25519}; @@ -33,6 +34,8 @@ use runtime_primitives::{ traits::{self, Extrinsic}, }; use transaction_pool::txpool::{Pool, ChainApi}; +use network::NetworkStateInfo; +use network::{PeerId, Multiaddr}; /// A message between the offchain extension and the processing thread. enum ExtMessage { @@ -59,6 +62,7 @@ pub(crate) struct Api { db: Storage, keys_password: Protected, key_provider: KeyProvider, + network_state: Arc, } fn unavailable_yet(name: &str) -> R { @@ -158,6 +162,26 @@ impl OffchainExt for Api where Ok(CryptoKeyId(id)) } + fn authority_pubkey(&self, kind: CryptoKind) -> Result, ()> { + let key = self.read_key(None, kind)?; + let public = match key { + Key::Sr25519(pair) => pair.public().encode(), + Key::Ed25519(pair) => pair.public().encode(), + }; + + Ok(public) + } + + fn network_state(&self) -> Result { + let external_addresses = self.network_state.external_addresses(); + + let state = NetworkState::new( + self.network_state.peer_id(), + external_addresses, + ); + Ok(OpaqueNetworkState::from(state)) + } + fn encrypt(&mut self, _key: Option, _kind: CryptoKind, _data: &[u8]) -> Result, ()> { unavailable_yet::<()>("encrypt"); Err(()) @@ -285,6 +309,71 @@ impl OffchainExt for Api where } } +/// Information about the local node's network state. +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct NetworkState { + peer_id: PeerId, + external_addresses: Vec, +} + +impl NetworkState { + fn new(peer_id: PeerId, external_addresses: Vec) -> Self { + NetworkState { + peer_id, + external_addresses, + } + } +} + +impl From for OpaqueNetworkState { + fn from(state: NetworkState) -> OpaqueNetworkState { + let enc = Encode::encode(&state.peer_id.into_bytes()); + let peer_id = OpaquePeerId::new(enc); + + let external_addresses: Vec = state + .external_addresses + .iter() + .map(|multiaddr| { + let e = Encode::encode(&multiaddr.to_string()); + OpaqueMultiaddr::new(e) + }) + .collect(); + + OpaqueNetworkState { + peer_id, + external_addresses, + } + } +} + +impl TryFrom for NetworkState { + type Error = (); + + fn try_from(state: OpaqueNetworkState) -> Result { + let inner_vec = state.peer_id.0; + + let bytes: Vec = Decode::decode(&mut &inner_vec[..]).ok_or(())?; + let peer_id = PeerId::from_bytes(bytes).map_err(|_| ())?; + + let external_addresses: Result, Self::Error> = state.external_addresses + .iter() + .map(|enc_multiaddr| -> Result { + let inner_vec = &enc_multiaddr.0; + let bytes = >::decode(&mut &inner_vec[..]).ok_or(())?; + let multiaddr_str = String::from_utf8(bytes).map_err(|_| ())?; + let multiaddr = Multiaddr::from_str(&multiaddr_str).map_err(|_| ())?; + Ok(multiaddr) + }) + .collect(); + let external_addresses = external_addresses?; + + Ok(NetworkState { + peer_id, + external_addresses, + }) + } +} + /// Offchain extensions implementation API /// /// This is the asynchronous processing part of the API. @@ -302,6 +391,7 @@ impl AsyncApi { keys_password: Protected, key_provider: P, at: BlockId, + network_state: Arc, ) -> (Api, AsyncApi) { let (sender, rx) = mpsc::unbounded(); @@ -310,6 +400,7 @@ impl AsyncApi { db, keys_password, key_provider, + network_state, }; let async_api = AsyncApi { @@ -355,8 +446,22 @@ impl AsyncApi { #[cfg(test)] mod tests { use super::*; + use std::{collections::HashSet, convert::TryFrom}; use client_db::offchain::LocalStorage; use crate::tests::TestProvider; + use network::PeerId; + + struct MockNetworkStateInfo(); + + impl NetworkStateInfo for MockNetworkStateInfo { + fn external_addresses(&self) -> Vec { + Vec::new() + } + + fn peer_id(&self) -> PeerId { + PeerId::random() + } + } fn offchain_api() -> (Api, AsyncApi) { let _ = env_logger::try_init(); @@ -366,7 +471,8 @@ mod tests { Pool::new(Default::default(), transaction_pool::ChainApi::new(client.clone())) ); - AsyncApi::new(pool, db, "pass".to_owned().into(), TestProvider::default(), BlockId::Number(0)) + let mock = Arc::new(MockNetworkStateInfo()); + AsyncApi::new(pool, db, "pass".to_owned().into(), TestProvider::default(), BlockId::Number(0), mock) } #[test] @@ -455,4 +561,23 @@ mod tests { "Invalid kind should trigger a missing key error." ); } + + #[test] + fn should_convert_network_states() { + // given + let state = NetworkState::new( + PeerId::random(), + vec![ + Multiaddr::try_from("/ip4/127.0.0.1/tcp/1234".to_string()).unwrap(), + Multiaddr::try_from("/ip6/2601:9:4f81:9700:803e:ca65:66e8:c21").unwrap(), + ], + ); + + // when + let opaque_state = OpaqueNetworkState::from(state.clone()); + let converted_back_state = NetworkState::try_from(opaque_state).unwrap(); + + // then + assert_eq!(state, converted_back_state); + } } diff --git a/substrate/core/offchain/src/lib.rs b/substrate/core/offchain/src/lib.rs index de3a5da084..f9eb3da07a 100644 --- a/substrate/core/offchain/src/lib.rs +++ b/substrate/core/offchain/src/lib.rs @@ -51,6 +51,7 @@ use runtime_primitives::{ }; use futures::future::Future; use transaction_pool::txpool::{Pool, ChainApi}; +use network::NetworkStateInfo; mod api; @@ -130,6 +131,7 @@ impl OffchainWorkers< &self, number: &::Number, pool: &Arc>, + network_state: Arc, ) -> impl Future where A: ChainApi + 'static, { @@ -145,6 +147,7 @@ impl OffchainWorkers< self.keys_password.clone(), self.authority_key.clone(), at.clone(), + network_state.clone(), ); debug!("Running offchain workers at {:?}", at); let api = Box::new(api); @@ -161,6 +164,20 @@ mod tests { use super::*; use futures::Future; use primitives::{ed25519, sr25519, crypto::{TypedKey, Pair}}; + use std::collections::HashSet; + use network::{Multiaddr, PeerId}; + + struct MockNetworkStateInfo(); + + impl NetworkStateInfo for MockNetworkStateInfo { + fn external_addresses(&self) -> Vec { + Vec::new() + } + + fn peer_id(&self) -> PeerId { + PeerId::random() + } + } #[derive(Clone, Default)] pub(crate) struct TestProvider { @@ -186,10 +203,11 @@ mod tests { let client = Arc::new(test_client::new()); let pool = Arc::new(Pool::new(Default::default(), ::transaction_pool::ChainApi::new(client.clone()))); let db = client_db::offchain::LocalStorage::new_test(); + let mock = Arc::new(MockNetworkStateInfo()); // when let offchain = OffchainWorkers::new(client, db, TestProvider::default(), "".to_owned().into()); - runtime.executor().spawn(offchain.on_block_imported(&0u64, &pool)); + runtime.executor().spawn(offchain.on_block_imported(&0u64, &pool, mock.clone())); // then runtime.shutdown_on_idle().wait().unwrap(); diff --git a/substrate/core/offchain/src/testing.rs b/substrate/core/offchain/src/testing.rs index 2d8c690e9a..f2fb7e14c4 100644 --- a/substrate/core/offchain/src/testing.rs +++ b/substrate/core/offchain/src/testing.rs @@ -31,6 +31,7 @@ use primitives::offchain::{ CryptoKind, CryptoKeyId, StorageKind, + OpaqueNetworkState, }; /// Pending request. @@ -139,6 +140,14 @@ impl offchain::Externalities for TestOffchainExt { unimplemented!("not needed in tests so far") } + fn network_state(&self) -> Result { + unimplemented!("not needed in tests so far") + } + + fn authority_pubkey(&self, _kind: CryptoKind) -> Result, ()> { + unimplemented!("not needed in tests so far") + } + fn new_crypto_key(&mut self, _crypto: CryptoKind) -> Result { unimplemented!("not needed in tests so far") } diff --git a/substrate/core/primitives/src/offchain.rs b/substrate/core/primitives/src/offchain.rs index 2ea93423d9..a13c26da26 100644 --- a/substrate/core/primitives/src/offchain.rs +++ b/substrate/core/primitives/src/offchain.rs @@ -185,6 +185,41 @@ impl TryFrom for HttpRequestStatus { } } +/// A blob to hold information about the local node's network state +/// without committing to its format. +#[derive(Clone, Eq, PartialEq, Encode, Decode)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct OpaqueNetworkState { + /// PeerId of the local node. + pub peer_id: OpaquePeerId, + /// List of addresses the node knows it can be reached as. + pub external_addresses: Vec, +} + +/// Simple blob to hold a `PeerId` without committing to its format. +#[derive(Clone, Eq, PartialEq, Encode, Decode)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct OpaquePeerId(pub Vec); + +impl OpaquePeerId { + /// Create new `OpaquePeerId` + pub fn new(vec: Vec) -> Self { + OpaquePeerId(vec) + } +} + +/// Simple blob to hold a `Multiaddr` without committing to its format. +#[derive(Clone, Eq, PartialEq, Encode, Decode)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct OpaqueMultiaddr(pub Vec); + +impl OpaqueMultiaddr { + /// Create new `OpaqueMultiaddr` + pub fn new(vec: Vec) -> Self { + OpaqueMultiaddr(vec) + } +} + /// Opaque timestamp type #[derive(Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Default)] #[cfg_attr(feature = "std", derive(Debug))] @@ -241,6 +276,12 @@ pub trait Externalities { /// The transaction will end up in the pool and be propagated to others. fn submit_transaction(&mut self, extrinsic: Vec) -> Result<(), ()>; + /// Returns information about the local node's network state. + fn network_state(&self) -> Result; + + /// Returns the locally configured authority public key, if available. + fn authority_pubkey(&self, crypto: CryptoKind) -> Result, ()>; + /// Create new key(pair) for signing/encryption/decryption. /// /// Returns an error if given crypto kind is not supported. @@ -319,7 +360,7 @@ pub trait Externalities { /// offchain worker tasks running on the same machine. It IS persisted between runs. fn local_storage_get(&mut self, kind: StorageKind, key: &[u8]) -> Option>; - /// Initiaties a http request given HTTP verb and the URL. + /// Initiates a http request given HTTP verb and the URL. /// /// Meta is a future-reserved field containing additional, parity-codec encoded parameters. /// Returns the id of newly started request. @@ -398,6 +439,14 @@ impl Externalities for Box { (&mut **self).encrypt(key, kind, data) } + fn network_state(&self) -> Result { + (& **self).network_state() + } + + fn authority_pubkey(&self, key:CryptoKind) -> Result, ()> { + (&**self).authority_pubkey(key) + } + fn decrypt(&mut self, key: Option, kind: CryptoKind, data: &[u8]) -> Result, ()> { (&mut **self).decrypt(key, kind, data) } diff --git a/substrate/core/service/Cargo.toml b/substrate/core/service/Cargo.toml index 8be5002d2d..6a15e2eeb6 100644 --- a/substrate/core/service/Cargo.toml +++ b/substrate/core/service/Cargo.toml @@ -33,6 +33,7 @@ transaction_pool = { package = "substrate-transaction-pool", path = "../../core/ rpc = { package = "substrate-rpc-servers", path = "../../core/rpc-servers" } tel = { package = "substrate-telemetry", path = "../../core/telemetry" } offchain = { package = "substrate-offchain", path = "../../core/offchain" } +parity-multiaddr = { package = "parity-multiaddr", version = "0.5.0" } [dev-dependencies] substrate-test-runtime-client = { path = "../test-runtime/client" } diff --git a/substrate/core/service/src/components.rs b/substrate/core/service/src/components.rs index 54c94730b0..1b0f9500a1 100644 --- a/substrate/core/service/src/components.rs +++ b/substrate/core/service/src/components.rs @@ -23,7 +23,7 @@ use client_db; use client::{self, Client, runtime_api}; use crate::{error, Service, AuthorityKeyProvider}; use consensus_common::{import_queue::ImportQueue, SelectChain}; -use network::{self, OnDemand, FinalityProofProvider, config::BoxFinalityProofRequestBuilder}; +use network::{self, OnDemand, FinalityProofProvider, NetworkStateInfo, config::BoxFinalityProofRequestBuilder}; use substrate_executor::{NativeExecutor, NativeExecutionDispatch}; use transaction_pool::txpool::{self, Options as TransactionPoolOptions, Pool as TransactionPool}; use runtime_primitives::{ @@ -235,6 +235,7 @@ pub trait OffchainWorker { ComponentBlock >, pool: &Arc>, + network_state: &Arc, ) -> error::Result + Send>>; } @@ -251,8 +252,9 @@ impl OffchainWorker for C where ComponentBlock >, pool: &Arc>, + network_state: &Arc, ) -> error::Result + Send>> { - Ok(Box::new(offchain.on_block_imported(number, pool))) + Ok(Box::new(offchain.on_block_imported(number, pool, network_state.clone()))) } } diff --git a/substrate/core/service/src/lib.rs b/substrate/core/service/src/lib.rs index 8da3c0c0f4..1ae5a0a58a 100644 --- a/substrate/core/service/src/lib.rs +++ b/substrate/core/service/src/lib.rs @@ -37,7 +37,7 @@ use exit_future::Signal; use futures::prelude::*; use futures03::stream::{StreamExt as _, TryStreamExt as _}; use keystore::Store as Keystore; -use network::NetworkState; +use network::{NetworkState, NetworkStateInfo}; use log::{info, warn, debug, error}; use parity_codec::{Encode, Decode}; use primitives::{Pair, ed25519, crypto}; @@ -293,6 +293,7 @@ impl Service { let wclient = Arc::downgrade(&client); let offchain = offchain_workers.as_ref().map(Arc::downgrade); let to_spawn_tx_ = to_spawn_tx.clone(); + let network_state_info: Arc = network.clone(); let events = client.import_notification_stream() .map(|v| Ok::<_, ()>(v)).compat() @@ -312,6 +313,7 @@ impl Service { &number, &offchain, &txpool, + &network_state_info, ).map_err(|e| warn!("Offchain workers error processing new block: {:?}", e))?; let _ = to_spawn_tx_.unbounded_send(future); } diff --git a/substrate/core/sr-io/src/lib.rs b/substrate/core/sr-io/src/lib.rs index f340f0a99b..4e5b9c9def 100644 --- a/substrate/core/sr-io/src/lib.rs +++ b/substrate/core/sr-io/src/lib.rs @@ -38,6 +38,7 @@ use primitives::offchain::{ HttpRequestId, HttpRequestStatus, HttpError, CryptoKind, CryptoKeyId, StorageKind, + OpaqueNetworkState, }; /// Error verifying ECDSA signature @@ -239,6 +240,13 @@ export_api! { /// The transaction will end up in the pool. fn submit_transaction(data: &T) -> Result<(), ()>; + /// Returns information about the local node's network state. + fn network_state() -> Result; + + /// Returns the currently configured authority public key, if available. + // TODO [#3139] change into crypto_pubkey(&self, key: Option, kind: CryptoKind) + fn authority_pubkey(crypto: CryptoKind) -> Result, ()>; + /// Create new key(pair) for signing/encryption/decryption. /// /// Returns an error if given crypto kind is not supported. @@ -313,7 +321,7 @@ export_api! { /// offchain worker tasks running on the same machine. It IS persisted between runs. fn local_storage_get(kind: StorageKind, key: &[u8]) -> Option>; - /// Initiaties a http request given HTTP verb and the URL. + /// Initiates a http request given HTTP verb and the URL. /// /// Meta is a future-reserved field containing additional, parity-codec encoded parameters. /// Returns the id of newly started request. diff --git a/substrate/core/sr-io/with_std.rs b/substrate/core/sr-io/with_std.rs index 9475aa0715..c6abef7cea 100644 --- a/substrate/core/sr-io/with_std.rs +++ b/substrate/core/sr-io/with_std.rs @@ -269,6 +269,18 @@ impl OffchainApi for () { }, "submit_transaction can be called only in the offchain worker context") } + fn network_state() -> Result { + with_offchain(|ext| { + ext.network_state() + }, "network_state can be called only in the offchain worker context") + } + + fn authority_pubkey(crypto: offchain::CryptoKind) -> Result, ()> { + with_offchain(|ext| { + ext.authority_pubkey(crypto) + }, "authority_pubkey can be called only in the offchain worker context") + } + fn new_crypto_key(crypto: offchain::CryptoKind) -> Result { with_offchain(|ext| { ext.new_crypto_key(crypto) diff --git a/substrate/core/sr-io/without_std.rs b/substrate/core/sr-io/without_std.rs index 5565f9ab38..9090d449f3 100644 --- a/substrate/core/sr-io/without_std.rs +++ b/substrate/core/sr-io/without_std.rs @@ -385,6 +385,33 @@ pub mod ext { /// - nonzero otherwise. fn ext_submit_transaction(data: *const u8, len: u32) -> u32; + /// Returns information about the local node's network state. + /// + /// # Returns + /// + /// The encoded `Result`. + /// `written_out` contains the length of the message. + /// + /// The ownership of the returned buffer is transferred to the runtime + /// code and the runtime is responsible for freeing it. This is always + /// a properly allocated pointer (which cannot be NULL), hence the + /// runtime code can always rely on it. + fn ext_network_state(written_out: *mut u32) -> *mut u8; + + /// Returns the locally configured authority public key, if available. + /// The `crypto` argument is `offchain::CryptoKind` converted to `u32`. + /// + /// # Returns + /// + /// The encoded `Result, ()>`. + /// `written_out` contains the length of the message. + /// + /// The ownership of the returned buffer is transferred to the runtime + /// code and the runtime is responsible for freeing it. This is always + /// a properly allocated pointer (which cannot be NULL), hence the + /// runtime code can always rely on it. + fn ext_authority_pubkey(crypto: u32, written_out: *mut u32) -> *mut u8; + /// Create new key(pair) for signing/encryption/decryption. /// /// # Returns @@ -504,7 +531,7 @@ pub mod ext { /// - Otherwise, pointer to the value in memory. `value_len` contains the length of the value. fn ext_local_storage_get(kind: u32, key: *const u8, key_len: u32, value_len: *mut u32) -> *mut u8; - /// Initiaties a http request. + /// Initiates a http request. /// /// `meta` is parity-codec encoded additional parameters to the request (like redirection policy, /// timeouts, certificates policy, etc). The format is not yet specified and the field is currently @@ -888,6 +915,39 @@ impl OffchainApi for () { } } + fn network_state() -> Result { + let mut len = 0_u32; + let raw_result = unsafe { + let ptr = ext_network_state.get()(&mut len); + + from_raw_parts(ptr, len) + }; + + match raw_result { + Some(raw_result) => codec::Decode::decode(&mut &*raw_result).unwrap_or(Err(())), + None => Err(()) + } + } + + fn authority_pubkey(kind: offchain::CryptoKind) -> Result, ()> { + let kind = kind as isize as u32; + + let mut len = 0u32; + let raw_result = unsafe { + let ptr = ext_authority_pubkey.get()( + kind, + &mut len, + ); + + from_raw_parts(ptr, len) + }; + + match raw_result { + Some(raw_result) => codec::Decode::decode(&mut &*raw_result).unwrap_or(Err(())), + None => Err(()) + } + } + fn new_crypto_key(crypto: offchain::CryptoKind) -> Result { let crypto = crypto.into(); let ret = unsafe { diff --git a/substrate/core/sr-primitives/src/generic/unchecked_extrinsic.rs b/substrate/core/sr-primitives/src/generic/unchecked_extrinsic.rs index d6e0d60e2c..6139139ce0 100644 --- a/substrate/core/sr-primitives/src/generic/unchecked_extrinsic.rs +++ b/substrate/core/sr-primitives/src/generic/unchecked_extrinsic.rs @@ -114,9 +114,15 @@ impl< Signature: Codec, Call, > Extrinsic for UncheckedExtrinsic { + type Call = Call; + fn is_signed(&self) -> Option { Some(self.signature.is_some()) } + + fn new_unsigned(call: Self::Call) -> Option { + Some(UncheckedExtrinsic::new_unsigned(call)) + } } impl Decode diff --git a/substrate/core/sr-primitives/src/generic/unchecked_mortal_compact_extrinsic.rs b/substrate/core/sr-primitives/src/generic/unchecked_mortal_compact_extrinsic.rs index 36e17fc277..7b50239a6f 100644 --- a/substrate/core/sr-primitives/src/generic/unchecked_mortal_compact_extrinsic.rs +++ b/substrate/core/sr-primitives/src/generic/unchecked_mortal_compact_extrinsic.rs @@ -59,9 +59,15 @@ impl UncheckedMortalCompactExtrinsic Extrinsic for UncheckedMortalCompactExtrinsic { + type Call = Call; + fn is_signed(&self) -> Option { Some(self.signature.is_some()) } + + fn new_unsigned(call: Self::Call) -> Option { + Some(UncheckedMortalCompactExtrinsic::new_unsigned(call)) + } } impl Checkable diff --git a/substrate/core/sr-primitives/src/generic/unchecked_mortal_extrinsic.rs b/substrate/core/sr-primitives/src/generic/unchecked_mortal_extrinsic.rs index 7f92b20edd..dda5a6cd58 100644 --- a/substrate/core/sr-primitives/src/generic/unchecked_mortal_extrinsic.rs +++ b/substrate/core/sr-primitives/src/generic/unchecked_mortal_extrinsic.rs @@ -61,9 +61,15 @@ impl UncheckedMortalExtrinsic Extrinsic for UncheckedMortalExtrinsic { + type Call = Call; + fn is_signed(&self) -> Option { Some(self.signature.is_some()) } + + fn new_unsigned(call: Self::Call) -> Option { + Some(UncheckedMortalExtrinsic::new_unsigned(call)) + } } impl Checkable diff --git a/substrate/core/sr-primitives/src/lib.rs b/substrate/core/sr-primitives/src/lib.rs index 5f0a888ccd..0b6321034c 100644 --- a/substrate/core/sr-primitives/src/lib.rs +++ b/substrate/core/sr-primitives/src/lib.rs @@ -869,9 +869,13 @@ impl ::serde::Serialize for OpaqueExtrinsic { } impl traits::Extrinsic for OpaqueExtrinsic { + type Call = (); + fn is_signed(&self) -> Option { None } + + fn new_unsigned(_call: Self::Call) -> Option { None } } #[cfg(test)] diff --git a/substrate/core/sr-primitives/src/testing.rs b/substrate/core/sr-primitives/src/testing.rs index f8df25ec59..c66d210c93 100644 --- a/substrate/core/sr-primitives/src/testing.rs +++ b/substrate/core/sr-primitives/src/testing.rs @@ -44,6 +44,14 @@ impl TypedKey for UintAuthorityId { const KEY_TYPE: KeyTypeId = UINT_DUMMY_KEY; } +impl AsRef<[u8]> for UintAuthorityId { + fn as_ref(&self) -> &[u8] { + let ptr = self.0 as *const _; + // It's safe to do this here since `UintAuthorityId` is `u64`. + unsafe { std::slice::from_raw_parts(ptr, 8) } + } +} + impl OpaqueKeys for UintAuthorityId { type KeyTypeIds = std::iter::Cloned>; @@ -133,6 +141,8 @@ impl<'a> Deserialize<'a> for Header { pub struct ExtrinsicWrapper(Xt); impl traits::Extrinsic for ExtrinsicWrapper { + type Call = (); + fn is_signed(&self) -> Option { None } @@ -219,6 +229,8 @@ impl Checkable for TestXt { fn check(self, _: &Context) -> Result { Ok(self) } } impl traits::Extrinsic for TestXt { + type Call = Call; + fn is_signed(&self) -> Option { Some(self.0.is_some()) } diff --git a/substrate/core/sr-primitives/src/traits.rs b/substrate/core/sr-primitives/src/traits.rs index a6d94babbe..2dbb3761d8 100644 --- a/substrate/core/sr-primitives/src/traits.rs +++ b/substrate/core/sr-primitives/src/traits.rs @@ -622,6 +622,12 @@ pub trait RandomnessBeacon { pub trait Member: Send + Sync + Sized + MaybeDebug + Eq + PartialEq + Clone + 'static {} impl Member for T {} +/// Determine if a `MemberId` is a valid member. +pub trait IsMember { + /// Is the given `MemberId` a valid member? + fn is_member(member_id: &MemberId) -> bool; +} + /// Something which fulfills the abstract idea of a Substrate header. It has types for a `Number`, /// a `Hash` and a `Digest`. It provides access to an `extrinsics_root`, `state_root` and /// `parent_hash`, as well as a `digest` and a block `number`. @@ -702,10 +708,16 @@ pub trait Block: Clone + Send + Sync + Codec + Eq + MaybeSerializeDebugButNotDes } /// Something that acts like an `Extrinsic`. -pub trait Extrinsic { +pub trait Extrinsic: Sized { + /// The function call. + type Call; + /// Is this `Extrinsic` signed? /// If no information are available about signed/unsigned, `None` should be returned. fn is_signed(&self) -> Option { None } + + /// New instance of an unsigned extrinsic aka "inherent". + fn new_unsigned(_call: Self::Call) -> Option { None } } /// Extract the hashing type for a block. diff --git a/substrate/core/state-machine/src/ext.rs b/substrate/core/state-machine/src/ext.rs index 4ade53a6f1..f6369159ba 100644 --- a/substrate/core/state-machine/src/ext.rs +++ b/substrate/core/state-machine/src/ext.rs @@ -81,7 +81,7 @@ where changes_trie_transaction: Option<(MemoryDB, H::Out)>, /// Additional externalities for offchain workers. /// - /// If None, some methods from the trait might not supported. + /// If None, some methods from the trait might not be supported. offchain_externalities: Option<&'a mut O>, /// Dummy usage of N arg. _phantom: ::std::marker::PhantomData, diff --git a/substrate/core/state-machine/src/lib.rs b/substrate/core/state-machine/src/lib.rs index f151fedaf5..769c774756 100644 --- a/substrate/core/state-machine/src/lib.rs +++ b/substrate/core/state-machine/src/lib.rs @@ -24,7 +24,7 @@ use log::warn; use hash_db::Hasher; use parity_codec::{Decode, Encode}; use primitives::{ - storage::well_known_keys, NativeOrEncoded, NeverNativeValue, offchain + storage::well_known_keys, NativeOrEncoded, NeverNativeValue, offchain, }; pub mod backend; @@ -240,6 +240,19 @@ impl offchain::Externalities for NeverOffchainExt { unreachable!() } + fn network_state( + &self, + ) -> Result { + unreachable!() + } + + fn authority_pubkey( + &self, + _crypto: offchain::CryptoKind, + ) -> Result, ()> { + unreachable!() + } + fn new_crypto_key( &mut self, _crypto: offchain::CryptoKind, diff --git a/substrate/core/test-runtime/src/lib.rs b/substrate/core/test-runtime/src/lib.rs index b72d2af62a..55c9f52c62 100644 --- a/substrate/core/test-runtime/src/lib.rs +++ b/substrate/core/test-runtime/src/lib.rs @@ -140,6 +140,8 @@ impl BlindCheckable for Extrinsic { } impl ExtrinsicT for Extrinsic { + type Call = (); + fn is_signed(&self) -> Option { if let Extrinsic::IncludeData(_) = *self { Some(false) @@ -147,6 +149,10 @@ impl ExtrinsicT for Extrinsic { Some(true) } } + + fn new_unsigned(_call: Self::Call) -> Option { + None + } } impl Extrinsic { diff --git a/substrate/node-template/runtime/src/lib.rs b/substrate/node-template/runtime/src/lib.rs index 11a477df96..26ff6f9ea9 100644 --- a/substrate/node-template/runtime/src/lib.rs +++ b/substrate/node-template/runtime/src/lib.rs @@ -80,9 +80,13 @@ pub mod opaque { } } impl traits::Extrinsic for UncheckedExtrinsic { + type Call = (); fn is_signed(&self) -> Option { None } + fn new_unsigned(_call: Self::Call) -> Option { + None + } } /// Opaque block header type. pub type Header = generic::Header; diff --git a/substrate/node/cli/src/chain_spec.rs b/substrate/node/cli/src/chain_spec.rs index 857eba782a..fc07ae4cea 100644 --- a/substrate/node/cli/src/chain_spec.rs +++ b/substrate/node/cli/src/chain_spec.rs @@ -19,10 +19,10 @@ use primitives::{ed25519, sr25519, Pair, crypto::UncheckedInto}; use node_primitives::{AccountId, AuraId, Balance}; use node_runtime::{ - GrandpaConfig, BalancesConfig, ContractsConfig, ElectionsConfig, DemocracyConfig, CouncilConfig, - AuraConfig, IndicesConfig, SessionConfig, StakingConfig, SudoConfig, TechnicalCommitteeConfig, - SystemConfig, WASM_BINARY, Perbill, SessionKeys, StakerStatus, DAYS, DOLLARS, - MILLICENTS, + GrandpaConfig, BalancesConfig, ContractsConfig, ElectionsConfig, DemocracyConfig, + CouncilConfig, AuraConfig, ImOnlineConfig, IndicesConfig, SessionConfig, StakingConfig, + SudoConfig, TechnicalCommitteeConfig, SystemConfig, WASM_BINARY, Perbill, SessionKeys, + StakerStatus, DAYS, DOLLARS, MILLICENTS, }; pub use node_runtime::GenesisConfig; use substrate_service; @@ -154,6 +154,10 @@ fn staging_testnet_config_genesis() -> GenesisConfig { aura: Some(AuraConfig { authorities: initial_authorities.iter().map(|x| x.2.clone()).collect(), }), + im_online: Some(ImOnlineConfig { + gossip_at: 0, + last_new_era_start: 0, + }), grandpa: Some(GrandpaConfig { authorities: initial_authorities.iter().map(|x| (x.3.clone(), 1)).collect(), }), @@ -291,6 +295,10 @@ pub fn testnet_genesis( aura: Some(AuraConfig { authorities: initial_authorities.iter().map(|x| x.2.clone()).collect(), }), + im_online: Some(ImOnlineConfig{ + gossip_at: 0, + last_new_era_start: 0, + }), grandpa: Some(GrandpaConfig { authorities: initial_authorities.iter().map(|x| (x.3.clone(), 1)).collect(), }), diff --git a/substrate/node/executor/src/lib.rs b/substrate/node/executor/src/lib.rs index 3ff2d876a2..5b2be48173 100644 --- a/substrate/node/executor/src/lib.rs +++ b/substrate/node/executor/src/lib.rs @@ -354,6 +354,7 @@ mod tests { gas_price: 1 * MILLICENTS, }), sudo: Some(Default::default()), + im_online: Some(Default::default()), grandpa: Some(GrandpaConfig { authorities: vec![], }), diff --git a/substrate/node/runtime/Cargo.toml b/substrate/node/runtime/Cargo.toml index 3bf28f7df2..17c666dbdc 100644 --- a/substrate/node/runtime/Cargo.toml +++ b/substrate/node/runtime/Cargo.toml @@ -33,6 +33,7 @@ system = { package = "srml-system", path = "../../srml/system", default-features timestamp = { package = "srml-timestamp", path = "../../srml/timestamp", default-features = false } treasury = { package = "srml-treasury", path = "../../srml/treasury", default-features = false } sudo = { package = "srml-sudo", path = "../../srml/sudo", default-features = false } +im-online = { package = "srml-im-online", path = "../../srml/im-online", default-features = false } node-primitives = { path = "../primitives", default-features = false } consensus_aura = { package = "substrate-consensus-aura-primitives", path = "../../core/consensus/aura/primitives", default-features = false } rustc-hex = { version = "2.0", optional = true } @@ -79,4 +80,5 @@ std = [ "rustc-hex", "substrate-keyring", "offchain-primitives/std", + "im-online/std", ] diff --git a/substrate/node/runtime/src/lib.rs b/substrate/node/runtime/src/lib.rs index d453e4f837..2f276580f4 100644 --- a/substrate/node/runtime/src/lib.rs +++ b/substrate/node/runtime/src/lib.rs @@ -74,8 +74,8 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // and set impl_version to equal spec_version. If only runtime // implementation changes and behavior does not, then leave spec_version as // is and increment impl_version. - spec_version: 114, - impl_version: 114, + spec_version: 115, + impl_version: 115, apis: RUNTIME_API_VERSIONS, }; @@ -193,7 +193,7 @@ parameter_types! { pub const Offset: BlockNumber = 0; } -type SessionHandlers = (Grandpa, Aura); +type SessionHandlers = (Grandpa, Aura, ImOnline); impl_opaque_keys! { pub struct SessionKeys { @@ -371,6 +371,15 @@ impl sudo::Trait for Runtime { type Proposal = Call; } +impl im_online::Trait for Runtime { + type AuthorityId = AuraId; + type Call = Call; + type Event = Event; + type SessionsPerEra = SessionsPerEra; + type UncheckedExtrinsic = UncheckedExtrinsic; + type IsValidAuthorityId = Aura; +} + impl grandpa::Trait for Runtime { type Event = Event; } @@ -409,6 +418,7 @@ construct_runtime!( Treasury: treasury::{Module, Call, Storage, Event}, Contracts: contracts, Sudo: sudo, + ImOnline: im_online::{default, ValidateUnsigned}, } ); diff --git a/substrate/srml/aura/src/lib.rs b/substrate/srml/aura/src/lib.rs index 1e92d411f4..da00f25476 100644 --- a/substrate/srml/aura/src/lib.rs +++ b/substrate/srml/aura/src/lib.rs @@ -54,7 +54,7 @@ use rstd::{result, prelude::*}; use parity_codec::Encode; use srml_support::{decl_storage, decl_module, Parameter, storage::StorageValue, traits::Get}; use primitives::{ - traits::{SaturatedConversion, Saturating, Zero, One, Member, TypedKey}, + traits::{SaturatedConversion, Saturating, Zero, One, Member, IsMember, TypedKey}, generic::DigestItem, }; use timestamp::OnTimestampSet; @@ -210,6 +210,14 @@ impl session::OneSessionHandler for Module { } } +impl IsMember for Module { + fn is_member(authority_id: &T::AuthorityId) -> bool { + Self::authorities() + .iter() + .any(|id| id == authority_id) + } +} + /// A report of skipped authorities in Aura. #[derive(Clone, PartialEq, Eq)] #[cfg_attr(feature = "std", derive(Debug))] diff --git a/substrate/srml/babe/src/lib.rs b/substrate/srml/babe/src/lib.rs index f1c8894a4d..8c7045675f 100644 --- a/substrate/srml/babe/src/lib.rs +++ b/substrate/srml/babe/src/lib.rs @@ -23,7 +23,10 @@ pub use timestamp; use rstd::{result, prelude::*}; use srml_support::{decl_storage, decl_module, StorageValue, traits::FindAuthor, traits::Get}; use timestamp::{OnTimestampSet, Trait}; -use primitives::{generic::DigestItem, traits::{SaturatedConversion, Saturating, RandomnessBeacon}}; +use primitives::{ + generic::DigestItem, + traits::{IsMember, SaturatedConversion, Saturating, RandomnessBeacon} +}; use primitives::ConsensusEngineId; #[cfg(feature = "std")] use timestamp::TimestampInherentData; @@ -188,6 +191,14 @@ impl FindAuthor for Module { } } +impl IsMember for Module { + fn is_member(authority_id: &AuthorityId) -> bool { + >::authorities() + .iter() + .any(|id| id == authority_id) + } +} + impl Module { /// Determine the BABE slot duration based on the Timestamp module configuration. pub fn slot_duration() -> T::Moment { diff --git a/substrate/srml/im-online/Cargo.toml b/substrate/srml/im-online/Cargo.toml new file mode 100644 index 0000000000..5ca5e0c533 --- /dev/null +++ b/substrate/srml/im-online/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "srml-im-online" +version = "0.1.0" +authors = ["Parity Technologies "] +edition = "2018" + +[dependencies] +parity-codec = { version = "4.1.1", default-features = false, features = ["derive"] } +primitives = { package = "sr-primitives", path = "../../core/sr-primitives", default-features = false } +rstd = { package = "sr-std", path = "../../core/sr-std", default-features = false } +serde = { version = "1.0", optional = true } +session = { package = "srml-session", path = "../session", default-features = false } +srml-support = { path = "../support", default-features = false } +sr-io = { package = "sr-io", path = "../../core/sr-io", default-features = false } +substrate_primitives = { package = "substrate-primitives", path = "../../core/primitives", default-features = false } +system = { package = "srml-system", path = "../system", default-features = false } + +[features] +default = ["std"] +std = [ + "parity-codec/std", + "primitives/std", + "rstd/std", + "serde", + "session/std", + "srml-support/std", + "sr-io/std", + "system/std", +] diff --git a/substrate/srml/im-online/src/lib.rs b/substrate/srml/im-online/src/lib.rs new file mode 100644 index 0000000000..26ca2f0581 --- /dev/null +++ b/substrate/srml/im-online/src/lib.rs @@ -0,0 +1,433 @@ +// Copyright 2019 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate 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. + +// Substrate 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 Substrate. If not, see . + +//! # I'm online Module +//! +//! If the local node is a validator (i.e. contains an authority key), this module +//! gossips a heartbeat transaction with each new session. The heartbeat functions +//! as a simple mechanism to signal that the node is online in the current era. +//! +//! Received heartbeats are tracked for one era and reset with each new era. The +//! module exposes two public functions to query if a heartbeat has been received +//! in the current era or session. +//! +//! The heartbeat is a signed transaction, which was signed using the session key +//! and includes the recent best block number of the local validators chain as well +//! as the [NetworkState](../../core/offchain/struct.NetworkState.html). +//! It is submitted as an Unsigned Transaction via off-chain workers. +//! +//! - [`im_online::Trait`](./trait.Trait.html) +//! - [`Call`](./enum.Call.html) +//! - [`Module`](./struct.Module.html) +//! +//! ## Interface +//! +//! ### Public Functions +//! +//! - `is_online_in_current_era` - True if the validator sent a heartbeat in the current era. +//! - `is_online_in_current_session` - True if the validator sent a heartbeat in the current session. +//! +//! ## Usage +//! +//! ``` +//! use srml_support::{decl_module, dispatch::Result}; +//! use system::ensure_signed; +//! use srml_im_online::{self as im_online}; +//! +//! pub trait Trait: im_online::Trait {} +//! +//! decl_module! { +//! pub struct Module for enum Call where origin: T::Origin { +//! pub fn is_online(origin, authority_id: T::AuthorityId) -> Result { +//! let _sender = ensure_signed(origin)?; +//! let _is_online = >::is_online_in_current_era(&authority_id); +//! Ok(()) +//! } +//! } +//! } +//! # fn main() { } +//! ``` +//! +//! ## Dependencies +//! +//! This module depends on the [Session module](../srml_session/index.html). + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +use substrate_primitives::{ + crypto::TypedKey, offchain::CryptoKind, + crypto::key_types, + offchain::OpaqueNetworkState, + offchain::StorageKind, + sr25519, ed25519, +}; +use parity_codec::{Encode, Decode}; +use primitives::{ + ApplyError, traits::{Member, IsMember, Extrinsic as ExtrinsicT}, + transaction_validity::{TransactionValidity, TransactionLongevity}, +}; +use rstd::prelude::*; +use session::SessionIndex; +use sr_io::Printable; +use srml_support::{ + Parameter, StorageValue, decl_module, decl_event, decl_storage, + traits::Get, StorageDoubleMap, print, +}; +use system::ensure_none; + +// The local storage database key under which the worker progress status +// is tracked. +const DB_KEY: &[u8] = b"srml/im-online-worker-status"; + +// It's important to persist the worker state, since e.g. the +// server could be restarted while starting the gossip process, but before +// finishing it. With every execution of the off-chain worker we check +// if we need to recover and resume gossipping or if there is already +// another off-chain worker in the process of gossipping. +#[derive(Encode, Decode, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(Debug))] +struct WorkerStatus { + done: bool, + gossipping_at: BlockNumber, +} + +// Error which may occur while executing the off-chain code. +enum OffchainErr { + DecodeAuthorityId, + DecodeWorkerStatus, + ExtrinsicCreation, + FailedSigning, + NetworkState, + SubmitTransaction, + UnknownCryptoKind, +} + +impl Printable for OffchainErr { + fn print(self) { + match self { + OffchainErr::DecodeAuthorityId => print("Offchain error: decoding AuthorityId failed!"), + OffchainErr::DecodeWorkerStatus => print("Offchain error: decoding WorkerStatus failed!"), + OffchainErr::ExtrinsicCreation => print("Offchain error: extrinsic creation failed!"), + OffchainErr::FailedSigning => print("Offchain error: signing failed!"), + OffchainErr::NetworkState => print("Offchain error: fetching network state failed!"), + OffchainErr::SubmitTransaction => print("Offchain error: submitting transaction failed!"), + OffchainErr::UnknownCryptoKind => print("Offchain error: the CryptoKind is unknown!"), + } + } +} + +/// Heartbeat which is send/received. +#[derive(Encode, Decode, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct Heartbeat + where BlockNumber: PartialEq + Eq + Decode + Encode, +{ + block_number: BlockNumber, + network_state: OpaqueNetworkState, + session_index: session::SessionIndex, + authority_id: AuthorityId, +} + +pub trait Trait: system::Trait + session::Trait { + /// The overarching event type. + type Event: From> + Into<::Event>; + + /// The function call. + type Call: From>; + + /// A extrinsic right from the external world. This is unchecked and so + /// can contain a signature. + type UncheckedExtrinsic: ExtrinsicT + Encode + Decode; + + /// The identifier type for an authority. + type AuthorityId: Member + Parameter + Default + TypedKey + Decode + Encode + AsRef<[u8]>; + + /// Number of sessions per era. + type SessionsPerEra: Get; + + /// Determine if an `AuthorityId` is a valid authority. + type IsValidAuthorityId: IsMember; +} + +decl_event!( + pub enum Event where + ::BlockNumber, + ::AuthorityId + { + /// A new heartbeat was received at this `BlockNumber` from `AuthorityId` + HeartbeatReceived(BlockNumber, AuthorityId), + } +); + +decl_storage! { + trait Store for Module as ImOnline { + // The block number when we should gossip. + GossipAt get(gossip_at) config(): T::BlockNumber; + + // The session index when the last new era started. + LastNewEraStart get(last_new_era_start) config(): Option; + + // For each session index we keep a mapping of `AuthorityId` to + // `offchain::OpaqueNetworkState`. + ReceivedHeartbeats get(received_heartbeats): double_map session::SessionIndex, + blake2_256(T::AuthorityId) => Vec; + } +} + +decl_module! { + pub struct Module for enum Call where origin: T::Origin { + /// Number of sessions per era. + const SessionsPerEra: session::SessionIndex = T::SessionsPerEra::get(); + + fn deposit_event() = default; + + fn heartbeat( + origin, + heartbeat: Heartbeat, + _signature: Vec + ) { + ensure_none(origin)?; + + let current_session = >::current_index(); + let exists = >::exists(current_session, &heartbeat.authority_id); + if !exists { + let now = >::block_number(); + Self::deposit_event(RawEvent::HeartbeatReceived(now, heartbeat.authority_id.clone())); + + let network_state = heartbeat.network_state.encode(); + >::insert(current_session, &heartbeat.authority_id, network_state); + } + } + + // Runs after every block. + fn offchain_worker(now: T::BlockNumber) { + fn gossip_at(block_number: T::BlockNumber) -> Result<(), OffchainErr> { + let kind = match ::KEY_TYPE { + key_types::SR25519 => CryptoKind::Sr25519, + key_types::ED25519 => CryptoKind::Ed25519, + _ => return Err(OffchainErr::UnknownCryptoKind), + }; + + // we run only when a local authority key is configured + if let Ok(key) = sr_io::authority_pubkey(kind) { + let authority_id = ::AuthorityId::decode(&mut &key[..]) + .ok_or(OffchainErr::DecodeAuthorityId)?; + let network_state = + sr_io::network_state().map_err(|_| OffchainErr::NetworkState)?; + let heartbeat_data = Heartbeat { + block_number, + network_state, + session_index: >::current_index(), + authority_id, + }; + + let signature = sr_io::sign(None, kind, &heartbeat_data.encode()) + .map_err(|_| OffchainErr::FailedSigning)?; + let call = Call::heartbeat(heartbeat_data, signature); + let ex = T::UncheckedExtrinsic::new_unsigned(call.into()) + .ok_or(OffchainErr::ExtrinsicCreation)?; + sr_io::submit_transaction(&ex) + .map_err(|_| OffchainErr::SubmitTransaction)?; + set_worker_status::(block_number, true); + } + Ok(()) + } + + fn set_worker_status(gossipping_at: T::BlockNumber, done: bool) { + let enc = WorkerStatus { + done, + gossipping_at, + }; + sr_io::local_storage_set(StorageKind::PERSISTENT, DB_KEY, &enc.encode()); + } + + fn was_not_yet_gossipped( + now: T::BlockNumber, + next_gossip: T::BlockNumber, + ) -> Result { + let last_gossip = sr_io::local_storage_get(StorageKind::PERSISTENT, DB_KEY); + match last_gossip { + Some(l) => { + let worker_status: WorkerStatus = Decode::decode(&mut &l[..]) + .ok_or(OffchainErr::DecodeWorkerStatus)?; + + let was_aborted = !worker_status.done && worker_status.gossipping_at < now; + + // another off-chain worker is currently in the process of submitting + let already_submitting = + !worker_status.done && worker_status.gossipping_at == now; + + let not_yet_gossipped = + worker_status.done && worker_status.gossipping_at < next_gossip; + + let ret = (was_aborted && !already_submitting) || not_yet_gossipped; + Ok(ret) + }, + None => Ok(true), + } + } + + let next_gossip = >::get(); + let not_yet_gossipped = match was_not_yet_gossipped::(now, next_gossip) { + Ok(v) => v, + Err(err) => { + print(err); + return; + }, + }; + if next_gossip < now && not_yet_gossipped { + set_worker_status::(now, false); + + match gossip_at::(now) { + Ok(_) => {}, + Err(err) => print(err), + } + } + } + } +} + +impl Module { + /// Returns `true` if a heartbeat has been received for `AuthorityId` + /// during the current era. Otherwise `false`. + pub fn is_online_in_current_era(authority_id: &T::AuthorityId) -> bool { + let curr = >::current_index(); + match LastNewEraStart::get() { + Some(start) => { + // iterate over every session + for index in start..curr { + if >::exists(index, authority_id) { + return true; + } + } + false + }, + None => >::exists(curr, authority_id), + } + } + + /// Returns `true` if a heartbeat has been received for `AuthorityId` + /// during the current session. Otherwise `false`. + pub fn is_online_in_current_session(authority_id: &T::AuthorityId) -> bool { + let current_session = >::current_index(); + >::exists(current_session, authority_id) + } + + /// Session has just changed. + fn new_session() { + let now = >::block_number(); + >::put(now); + + let current_session = >::current_index(); + + match LastNewEraStart::get() { + Some(last_new_era_start) => { + let sessions_per_era = T::SessionsPerEra::get(); + + let new_era = current_session - last_new_era_start > sessions_per_era; + if new_era { + LastNewEraStart::put(current_session); + Self::remove_heartbeats(); + } + }, + None => LastNewEraStart::put(current_session), + }; + } + + // Remove all stored heartbeats. + fn remove_heartbeats() { + let curr = >::current_index(); + match LastNewEraStart::get() { + Some(start) => { + for index in start..curr { + >::remove_prefix(index); + } + }, + None => >::remove_prefix(curr), + } + } +} + +impl session::OneSessionHandler for Module { + type Key = ::AuthorityId; + + fn on_new_session<'a, I: 'a>(_changed: bool, _validators: I) { + Self::new_session(); + } + + fn on_disabled(_i: usize) { + // ignore + } +} + +impl srml_support::unsigned::ValidateUnsigned for Module { + type Call = Call; + + fn validate_unsigned(call: &Self::Call) -> srml_support::unsigned::TransactionValidity { + if let Call::heartbeat(heartbeat, signature) = call { + // verify that the incoming (unverified) pubkey is actually an authority id + let is_authority = T::IsValidAuthorityId::is_member(&heartbeat.authority_id); + if !is_authority { + return TransactionValidity::Invalid(ApplyError::BadSignature as i8); + } + + if >::is_online_in_current_session(&heartbeat.authority_id) { + // we already received a heartbeat for this authority + return TransactionValidity::Invalid(ApplyError::BadSignature as i8); + } + + if signature.len() != 64 { + return TransactionValidity::Invalid(ApplyError::BadSignature as i8); + } + + let signature = { + let mut array = [0; 64]; + array.copy_from_slice(&signature); // panics if not enough, hence the check above + array + }; + + let encoded_heartbeat = heartbeat.encode(); + + let signature_valid = match ::KEY_TYPE { + ed25519::Public::KEY_TYPE => + sr_io::ed25519_verify(&signature, &encoded_heartbeat, &heartbeat.authority_id), + sr25519::Public::KEY_TYPE => + sr_io::sr25519_verify(&signature, &encoded_heartbeat, &heartbeat.authority_id), + _ => return TransactionValidity::Invalid(ApplyError::BadSignature as i8), + }; + + if !signature_valid { + return TransactionValidity::Invalid(ApplyError::BadSignature as i8); + } + + // check if session index from heartbeat is recent + let current_session = >::current_index(); + if heartbeat.session_index < current_session { + return TransactionValidity::Invalid(ApplyError::BadSignature as i8); + } + + return srml_support::unsigned::TransactionValidity::Valid { + priority: 0, + requires: vec![], + provides: vec![encoded_heartbeat], + longevity: TransactionLongevity::max_value(), + propagate: true, + } + } + TransactionValidity::Invalid(0) + } +}