Conversation
core/cli/src/lib.rs
Outdated
| syncing: cli.syncing_execution.into(), | ||
| importing: cli.importing_execution.into(), | ||
| block_construction: cli.block_construction_execution.into(), | ||
| offchain_worker: ExecutionStrategy::NativeWhenPossible, |
There was a problem hiding this comment.
Should it always be NativeWhenPossible or configurable by the user at some point?
There was a problem hiding this comment.
Could be configurable primarily to keep it consistent with all the other wasm execution. But I don't see any instance where you would not want to use native if it's there.
Maybe in debugging...
There was a problem hiding this comment.
Yeah, I was not sure as well, but if it does not need to be configurable, we don't need to touch this struct at all.
There was a problem hiding this comment.
I'll add the option for consistency.
There was a problem hiding this comment.
given that native workers are not consensus critical, NativeWhenPossible seems like the most reasonable strategy
| /// or to the next produced block (inherent). | ||
| fn submit_extrinsic(&mut self, extrinsic: Vec<u8>); | ||
| } | ||
| impl<T: OffchainExt + ?Sized> OffchainExt for Box<T> { |
There was a problem hiding this comment.
Why do wen need this implementation? Box supports DerefMut, shouldn't that be enough?
There was a problem hiding this comment.
Yeah, but DerefMut just means that we can use functions from that trait directly on Box<T>, we need to pass Box<T> to a generic function accepting T: OffchainExt though.
I wasn't sure about the API initially and chose to handle generic parameters instead of Box, but currently the only way we actually pass the offchain extensions is by ExecutionContext, and making it generic was introducing too much noise in the code so I chose to Box, but only there.
Let me know if you think it's better to change the other apis and structs to store Box instead of generic param.
core/offchain/Cargo.toml
Outdated
| [dependencies] | ||
| client = { package = "substrate-client", path = "../../core/client" } | ||
| consensus = { package = "substrate-consensus-common", path = "../../core/consensus/common" } | ||
| futures = "0.1" |
There was a problem hiding this comment.
| futures = "0.1" | |
| futures = "0.1.25" |
There was a problem hiding this comment.
I don't see why it's better. Cargo.lock manages the concrete version anyway and it's just misleading to see 0.1.17 in some other packages (like core/client/Cargo.toml) while after all 0.1.25 is used.
There was a problem hiding this comment.
Cargo.lock manages the concrete version anyway
not on downstream crates. we've been bitten by that before.
You should use futures = "0.1.x" where x is whatever you need for the features you use in this crate. Probably some of them were added after 0.1 -- your Cargo.toml says that any futures crate including 0.1 is supposed to work.
The best strategy is as Basti suggested; just use the latest patch version.
There was a problem hiding this comment.
Yeah, I forgot that substrate is used as a library, I'm still not convinced though that it's a good strategy to just use latest patch version, the minimal futures version might be just managed in a single place (say service), cause one crate in the dependency tree is enough to define the minimal required version (specifying latest patch might be overly restrictive), that would also give us some consistency in other crates, right now it seems mostly random.
There was a problem hiding this comment.
I should have said the easiest strategy, not best :)
core/offchain/Cargo.toml
Outdated
| authors = ["Parity Technologies <admin@parity.io>"] | ||
| edition = "2018" | ||
|
|
||
| [lib] |
core/client/src/client.rs
Outdated
| || self.prepare_environment_block(at), | ||
| manager, | ||
| native_call, | ||
| Some(&mut ext), |
There was a problem hiding this comment.
Can we not just do:
match context {
ExecutionContext::OffchainWorker(mut ext) => Some(&mut ext),
_ => None,
}
or something comparable, instead of cloning this function call?
There was a problem hiding this comment.
Yeah, we can do that although it will result in a different type of Option<>. The NeverOffchainExt should be optimized out by the compiler, since it's not instantiable, but I guess it won't matter that much.
core/offchain/primitives/src/lib.rs
Outdated
| #![warn(missing_docs)] | ||
|
|
||
| use client::decl_runtime_apis; | ||
| use runtime_primitives::traits::{Header as HeaderT, Block as BlockT}; |
There was a problem hiding this comment.
| use runtime_primitives::traits::{Header as HeaderT, Block as BlockT}; | |
| use runtime_primitives::traits::NumberFor; |
core/offchain/src/lib.rs
Outdated
| let has_api = runtime.has_api::<OffchainWorkerApi<Block>>(&at); | ||
| debug!("Checking offchain workers at {:?}: {:?}", at, has_api); | ||
|
|
||
| if let Ok(true) = has_api { |
There was a problem hiding this comment.
| if let Ok(true) = has_api { | |
| if has_api.unwrap_or(false) { |
core/sr-io/without_std.rs
Outdated
| /// Depending on the kind of extrinsic it will either be: | ||
| /// 1. scheduled to be included in the next produced block (inherent) | ||
| /// 2. added to the pool and propagated (transaction) | ||
| pub fn submit_extrinsic(data: Vec<u8>) { |
There was a problem hiding this comment.
Do we maybe want to switch to the following interface?
pub fn submit_extrinsic<T: Encode>(data: T) {
| Ok(0) | ||
| }, | ||
| ext_submit_extrinsic(msg_data: *const u8, len: u32) => { | ||
| let extrinsic = this.memory.get(msg_data, len as usize) |
There was a problem hiding this comment.
Do we maybe want to fail, when someone tries to call it on-chain?
There was a problem hiding this comment.
this.ext.submit_extrinsic is going to fail or (as currently) trigger a warning. We can either panic (which I'm not a fan of in consensus code) or just return an Error?
There was a problem hiding this comment.
Yeah return an Error. I don't know what happens with this error, but we will see it :D
There was a problem hiding this comment.
Spoke with Sergei and errors are converted to traps in the wasm environment, so I've also added a panicking implementation in case of std environment.
There was a problem hiding this comment.
Spoke with Sergei and the Errors are converted to traps in wasm environment so I've also added a panicking implementation in sr_io::with_std
| } | ||
|
|
||
| fn submit_extrinsic(&mut self, extrinsic: Vec<u8>) -> Result<(), ()> { | ||
| let _guard = panic_handler::AbortGuard::new(true); |
There was a problem hiding this comment.
I could possibly be wrong, but could you, please, verify @tomusdrw :
- most of times runtime calls are performed with
NeverOffchainExt; NeverOffchainExt::submit_extrinsicis simplyunreachable!();- this guard means that if we met
panic!(),unreachable!(), etc in the code, the process willexit(1)(seepanic_handler); - the runtime could try to call
ext_submit_extrinsicanytime (i.e. frominitialise_block), and, given (1)..(3) the process will exit with 1 instead of failing execution?
There was a problem hiding this comment.
Oh, wait :) NeverOffchainExt::new returns None, so we will go to else branch here :)
core/offchain/src/lib.rs
Outdated
| // You should have received a copy of the GNU General Public License | ||
| // along with Substrate. If not, see <http://www.gnu.org/licenses/>. | ||
|
|
||
| //! Substrate service. Starts a thread that spins up the network, client, and extrinsic pool. |
There was a problem hiding this comment.
Looks like docs are for substrate_service::Service
core/state-machine/src/ext.rs
Outdated
| /// data at this moment (block number). | ||
| changes_trie_transaction: Option<(u64, MemoryDB<H>, H::Out)>, | ||
| /// Additional externalities for offchain workers. | ||
| /// If None the some methods from the trait might not supported. |
There was a problem hiding this comment.
| /// If None the some methods from the trait might not supported. | |
| /// If None some methods from the trait might not supported. |
My english is bad but this is a typo isn't it ?
pepyakin
left a comment
There was a problem hiding this comment.
Looks good to me! A few nits and one Q
|
|
||
| let changes_trie_storage = InMemoryChangesTrieStorage::new(); | ||
| let mut ext = Ext::new(&mut overlay, &backend, Some(&changes_trie_storage)); | ||
| let mut ext = Ext::new(&mut overlay, &backend, Some(&changes_trie_storage), crate::NeverOffchainExt::new()); |
There was a problem hiding this comment.
Can we wrap this?
| let mut ext = Ext::new(&mut overlay, &backend, Some(&changes_trie_storage), crate::NeverOffchainExt::new()); | |
| let mut ext = Ext::new( | |
| &mut overlay, | |
| &backend, | |
| Some(&changes_trie_storage), | |
| crate::NeverOffchainExt::new(), | |
| ); |
core/state-machine/src/lib.rs
Outdated
| { | ||
| let mut externalities = ext::Ext::new(self.overlay, self.backend, self.changes_trie_storage); | ||
| let offchain = self.offchain_ext.as_mut(); | ||
| let mut externalities = ext::Ext::new(self.overlay, self.backend, self.changes_trie_storage, offchain.map(|x| &mut **x)); |
There was a problem hiding this comment.
Can we wrap this?
| let mut externalities = ext::Ext::new(self.overlay, self.backend, self.changes_trie_storage, offchain.map(|x| &mut **x)); | |
| let mut externalities = ext::Ext::new( | |
| self.overlay, | |
| self.backend, | |
| self.changes_trie_storage, | |
| offchain.map(|x| &mut **x), | |
| ); |
| /// | ||
| /// The extrinsic will either go to the pool (signed) | ||
| /// or to the next produced block (inherent). | ||
| fn submit_extrinsic(&mut self, extrinsic: Vec<u8>); |
There was a problem hiding this comment.
I think it would be cool if we introduced a type alias for an OpaqueExtrinsic. This way it will be a) more apparent from the code that we deal with an extrinsic and b) will allow us to add some docs on why we use Vec for representing an extrinsic.
(Not to be addressed in this PR)
There was a problem hiding this comment.
Yeah, I'm going to address that when valid_until parameter is introduced, then this will be a common type for Vec<u8> and BlockNumber
core/cli/src/lib.rs
Outdated
| config.offchain_worker = match (cli.offchain_worker, role) { | ||
| (params::OffchainWorkerEnabled::WhenValidating, service::Roles::AUTHORITY) => true, | ||
| (params::OffchainWorkerEnabled::Always, _) => true, | ||
| _ => false, |
There was a problem hiding this comment.
Let's use exhaustive matching so we don't trip up accidently if we add a new variant here.
| _ => false, | |
| (params::OffchainWorkerEnabled::Never, _) => false, |
|
|
||
| if has_api.unwrap_or(false) { | ||
| let (api, runner) = api::Api::new(pool.clone(), self.inherents_pool.clone(), at.clone()); | ||
| self.executor.spawn(runner.process()); |
There was a problem hiding this comment.
Sorry I don't know much about Rust futures, but can you ELI5 how this terminates?
Is the receiver returns Ready when tx is dropped?
There was a problem hiding this comment.
Yeah, so when the work runtime is doing is finished the api transmitting end will be dropped, that in turn will cause the receiving.for_each stream to be terminated, so the entire future returned from process() will terminate.
But we currently allow the offchain workers to run arbitrarily long, it might make sense actually to kill them after some timeout, but that's something I don't really know how to do:
- I think it's not possible to terminate wasm task currently
- We could error when the worker attempts to use
ext_submit_extrinsic(or even any other externality), but obviously it can also never happen.
| A: ChainApi<Block=Block> + 'static, | ||
| { | ||
| let runtime = self.client.runtime_api(); | ||
| let at = BlockId::number(*number); |
There was a problem hiding this comment.
One last question: why block is identified by number (i.e. not the hash)? What if not best block is imported? Then the offchain worker could be executed with state of some other block (and probably it could be executed several times if there are several competing forks of the height number). Is this intended?
There was a problem hiding this comment.
Yes, I think that's expected. Since the offchain worker produces inherents for next block they might look different if an alternative fork is imported.
We might implement a way to invalidate previously generated inherents/extrinsics though, however
offchain workers are also supposed to have a local storage in the future, so two "competing" tasks could have some sort of communication between runs i.e. the second run for block at height X will be able to tell it's on a fork that now became canon and for instance submit extrinsics that will replace the previous ones.
Numeric block number will also later be used by the offchain worker code to calculate lifetime of produced extrinsics (i.e. you can say I submit this inherent after performing a super-long-running computation, but it's only valid before block bn + 3), or you can also use that to execute some computation every X blocks.
* Refactor state-machine stuff. * Fix tests. * WiP * WiP2 * Service support for offchain workers. * Service support for offchain workers. * Testing offchain worker. * Initial version working. * Pass side effects in call. * Pass OffchainExt in context. * Submit extrinsics to the pool. * Support inherents. * Insert to inherents pool. * Inserting to the pool asynchronously. * Add test to offchain worker. * Implement convenience syntax for modules. * Dispatching offchain worker through executive. * Fix offchain test. * Remove offchain worker from timestamp. * Update Cargo.lock. * Address review comments. * Use latest patch version for futures. * Add CLI parameter for offchain worker. * Fix compilation. * Fix test. * Fix extrinsics format for tests. * Fix RPC test. * Bump spec version. * Fix executive. * Fix support macro. * Address grumbles. * Bump runtime
|
This pull request has been mentioned on Polkadot Forum. There might be relevant details there: https://forum.polkadot.network/t/offchain-workers-design-assumptions-vulnerabilities/2548/1 |
Initial version of offchain workers (#1458)
fn offchain_worker()function indecl_module(alikeon_initialise)Executivemodule to dispatch the call to all modules.Contextof execution, it means that additional externalities are available for those functions (currently onlysubmit_extrinsic).InherentsPooland are included as extrinsics (after regular inherents, but before transactions) when new block is created.Additional work to folllow:
OpaqueTransactionforsubmit_extrinsictransaction type