Persistent Local Storage for offchain workers. (#2894)

* WiP.

* Implement offchain storage APIs.

* Change compare_and_set to return bool.

* Add offchain http test.

* Fix tests.

* Bump spec version.

* Fix warnings and test.

* Fix compilation.

* Remove unused code.

* Introduce Local (fork-aware) and Persistent storage.

* Fix borked merge.

* Prevent warning on depreacated client.backend

* Fix long lines.

* Clean up dependencies.

* Update core/primitives/src/offchain.rs

Co-Authored-By: André Silva <andre.beat@gmail.com>

* Update core/primitives/src/offchain.rs

Co-Authored-By: André Silva <andre.beat@gmail.com>
This commit is contained in:
Tomasz Drwięga
2019-07-02 20:11:06 +02:00
committed by Gavin Wood
parent 24aa882ebc
commit 2217c1e9a1
26 changed files with 726 additions and 118 deletions
+3 -1
View File
@@ -4244,6 +4244,8 @@ dependencies = [
"parking_lot 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"sr-io 2.0.0",
"sr-version 2.0.0",
"substrate-client 2.0.0",
"substrate-offchain 2.0.0",
"substrate-panic-handler 2.0.0",
"substrate-primitives 2.0.0",
"substrate-serializer 2.0.0",
@@ -4386,7 +4388,7 @@ dependencies = [
"parking_lot 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"sr-primitives 2.0.0",
"substrate-client 2.0.0",
"substrate-consensus-common 2.0.0",
"substrate-client-db 2.0.0",
"substrate-offchain-primitives 2.0.0",
"substrate-primitives 2.0.0",
"substrate-test-runtime-client 2.0.0",
+31 -15
View File
@@ -24,7 +24,10 @@
//!
//! Finality implies canonicality but not vice-versa.
#![warn(missing_docs)]
pub mod light;
pub mod offchain;
mod cache;
mod storage_cache;
@@ -77,6 +80,10 @@ const DEFAULT_CHILD_RATIO: (usize, usize) = (1, 10);
/// DB-backed patricia trie state, transaction type is an overlay of changes to commit.
pub type DbState = state_machine::TrieBackend<Arc<dyn state_machine::Storage<Blake2Hasher>>, Blake2Hasher>;
/// A reference tracking state.
///
/// It makes sure that the hash we are using stays pinned in storage
/// until this structure is dropped.
pub struct RefTrackingState<Block: BlockT> {
state: DbState,
storage: Arc<StorageDb<Block>>,
@@ -202,7 +209,7 @@ pub fn new_client<E, S, Block, RA>(
Ok(client::Client::new(backend, executor, genesis_storage, execution_strategies)?)
}
mod columns {
pub(crate) mod columns {
pub const META: Option<u32> = crate::utils::COLUMN_META;
pub const STATE: Option<u32> = Some(1);
pub const STATE_META: Option<u32> = Some(2);
@@ -213,6 +220,8 @@ mod columns {
pub const JUSTIFICATION: Option<u32> = Some(6);
pub const CHANGES_TRIE: Option<u32> = Some(7);
pub const AUX: Option<u32> = Some(8);
/// Offchain workers local storage
pub const OFFCHAIN: Option<u32> = Some(9);
}
struct PendingBlock<Block: BlockT> {
@@ -531,6 +540,7 @@ impl state_machine::Storage<Blake2Hasher> for DbGenesisStorage {
}
}
/// A database wrapper for changes tries.
pub struct DbChangesTrieStorage<Block: BlockT> {
db: Arc<dyn KeyValueDB>,
meta: Arc<RwLock<Meta<NumberFor<Block>, Block::Hash>>>,
@@ -675,6 +685,7 @@ where
/// Otherwise, trie nodes are kept only from some recent blocks.
pub struct Backend<Block: BlockT> {
storage: Arc<StorageDb<Block>>,
offchain_storage: offchain::LocalStorage,
changes_tries_storage: DbChangesTrieStorage<Block>,
/// None<*> means that the value hasn't been cached yet. Some(*) means that the value (either None or
/// Some(*)) has been cached and is valid.
@@ -689,31 +700,29 @@ impl<Block: BlockT<Hash=H256>> Backend<Block> {
/// Create a new instance of database backend.
///
/// The pruning window is how old a block must be before the state is pruned.
pub fn new(config: DatabaseSettings, canonicalization_delay: u64) -> Result<Self, client::error::Error> {
pub fn new(config: DatabaseSettings, canonicalization_delay: u64) -> client::error::Result<Self> {
Self::new_inner(config, canonicalization_delay)
}
#[cfg(feature = "kvdb-rocksdb")]
fn new_inner(config: DatabaseSettings, canonicalization_delay: u64) -> Result<Self, client::error::Error> {
#[cfg(feature = "kvdb-rocksdb")]
let db = crate::utils::open_database(&config, columns::META, "full")?;
Backend::from_kvdb(db as Arc<_>, canonicalization_delay, &config)
}
#[cfg(not(feature = "kvdb-rocksdb"))]
fn new_inner(config: DatabaseSettings, canonicalization_delay: u64) -> Result<Self, client::error::Error> {
log::warn!("Running without the RocksDB feature. The database will NOT be saved.");
let db = Arc::new(kvdb_memorydb::create(crate::utils::NUM_COLUMNS));
Backend::from_kvdb(db as Arc<_>, canonicalization_delay, &config)
#[cfg(not(feature = "kvdb-rocksdb"))]
let db = {
log::warn!("Running without the RocksDB feature. The database will NOT be saved.");
Arc::new(kvdb_memorydb::create(crate::utils::NUM_COLUMNS))
};
Self::from_kvdb(db as Arc<_>, canonicalization_delay, &config)
}
/// Create new memory-backed client backend for tests.
#[cfg(any(test, feature = "test-helpers"))]
pub fn new_test(keep_blocks: u32, canonicalization_delay: u64) -> Self {
use utils::NUM_COLUMNS;
let db = Arc::new(::kvdb_memorydb::create(NUM_COLUMNS));
let db = Arc::new(kvdb_memorydb::create(crate::utils::NUM_COLUMNS));
Self::new_test_db(keep_blocks, canonicalization_delay, db as Arc<_>)
}
/// Creates a client backend with test settings.
#[cfg(any(test, feature = "test-helpers"))]
pub fn new_test_db(keep_blocks: u32, canonicalization_delay: u64, db: Arc<dyn KeyValueDB>) -> Self {
@@ -724,7 +733,7 @@ impl<Block: BlockT<Hash=H256>> Backend<Block> {
path: Default::default(),
pruning: PruningMode::keep_blocks(keep_blocks),
};
Backend::from_kvdb(
Self::from_kvdb(
db,
canonicalization_delay,
&db_setting,
@@ -745,6 +754,7 @@ impl<Block: BlockT<Hash=H256>> Backend<Block> {
db: db.clone(),
state_db,
};
let offchain_storage = offchain::LocalStorage::new(db.clone());
let changes_tries_storage = DbChangesTrieStorage {
db,
meta,
@@ -754,6 +764,7 @@ impl<Block: BlockT<Hash=H256>> Backend<Block> {
Ok(Backend {
storage: Arc::new(storage_db),
offchain_storage,
changes_tries_storage,
changes_trie_config: Mutex::new(None),
blockchain,
@@ -1232,6 +1243,7 @@ impl<Block> client::backend::Backend<Block, Blake2Hasher> for Backend<Block> whe
type Blockchain = BlockchainDb<Block>;
type State = CachingState<Blake2Hasher, RefTrackingState<Block>, Block>;
type ChangesTrieStorage = DbChangesTrieStorage<Block>;
type OffchainStorage = offchain::LocalStorage;
fn begin_operation(&self) -> Result<Self::BlockImportOperation, client::error::Error> {
let old_state = self.state_at(BlockId::Hash(Default::default()))?;
@@ -1305,6 +1317,10 @@ impl<Block> client::backend::Backend<Block, Blake2Hasher> for Backend<Block> whe
Some(&self.changes_tries_storage)
}
fn offchain_storage(&self) -> Option<Self::OffchainStorage> {
Some(self.offchain_storage.clone())
}
fn revert(&self, n: NumberFor<Block>) -> Result<NumberFor<Block>, client::error::Error> {
let mut best = self.blockchain.info().best_number;
let finalized = self.blockchain.info().finalized_number;
+1
View File
@@ -84,6 +84,7 @@ impl<Block> LightStorage<Block>
Self::from_kvdb(db as Arc<_>)
}
/// Create new memory-backed `LightStorage` for tests.
#[cfg(any(test, feature = "test-helpers"))]
pub fn new_test() -> Self {
use utils::NUM_COLUMNS;
+137
View File
@@ -0,0 +1,137 @@
// Copyright 2017-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 <http://www.gnu.org/licenses/>.
//! RocksDB-based offchain workers local storage.
use std::{
collections::HashMap,
sync::Arc,
};
use crate::columns;
use kvdb::KeyValueDB;
use parking_lot::Mutex;
/// Offchain local storage
#[derive(Clone)]
pub struct LocalStorage {
db: Arc<KeyValueDB>,
locks: Arc<Mutex<HashMap<Vec<u8>, Arc<Mutex<()>>>>>,
}
impl std::fmt::Debug for LocalStorage {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
fmt.debug_struct("LocalStorage")
.finish()
}
}
impl LocalStorage {
/// Create new offchain storage for tests (backed by memorydb)
#[cfg(any(test, feature = "test-helpers"))]
pub fn new_test() -> Self {
let db = Arc::new(::kvdb_memorydb::create(crate::utils::NUM_COLUMNS));
Self::new(db as _)
}
/// Create offchain local storage with given `KeyValueDB` backend.
pub fn new(db: Arc<KeyValueDB>) -> Self {
Self {
db,
locks: Default::default(),
}
}
}
impl client::backend::OffchainStorage for LocalStorage {
fn set(&mut self, prefix: &[u8], key: &[u8], value: &[u8]) {
let key: Vec<u8> = prefix.iter().chain(key).cloned().collect();
let mut tx = self.db.transaction();
tx.put(columns::OFFCHAIN, &key, value);
if let Err(e) = self.db.write(tx) {
log::warn!("Error writing to the offchain DB: {:?}", e);
}
}
fn get(&self, prefix: &[u8], key: &[u8]) -> Option<Vec<u8>> {
let key: Vec<u8> = prefix.iter().chain(key).cloned().collect();
self.db.get(columns::OFFCHAIN, &key)
.ok()
.and_then(|x| x)
.map(|v| v.to_vec())
}
fn compare_and_set(
&mut self,
prefix: &[u8],
item_key: &[u8],
old_value: &[u8],
new_value: &[u8],
) -> bool {
let key: Vec<u8> = prefix.iter().chain(item_key).cloned().collect();
let key_lock = {
let mut locks = self.locks.lock();
locks.entry(key.clone()).or_default().clone()
};
let is_set;
{
let _key_guard = key_lock.lock();
is_set = self.db.get(columns::OFFCHAIN, &key)
.ok()
.and_then(|x| x)
.map(|v| &*v == old_value)
.unwrap_or(true);
if is_set {
self.set(prefix, item_key, new_value)
}
}
// clean the lock map if we're the only entry
let mut locks = self.locks.lock();
{
drop(key_lock);
let key_lock = locks.get_mut(&key);
if let Some(_) = key_lock.and_then(Arc::get_mut) {
locks.remove(&key);
}
}
is_set
}
}
#[cfg(test)]
mod tests {
use super::*;
use client::backend::OffchainStorage;
#[test]
fn should_compare_and_set_and_clear_the_locks_map() {
let mut storage = LocalStorage::new_test();
let prefix = b"prefix";
let key = b"key";
let value = b"value";
storage.set(prefix, key, value);
assert_eq!(storage.get(prefix, key), Some(value.to_vec()));
assert_eq!(storage.compare_and_set(prefix, key, value, b"asd"), true);
assert_eq!(storage.get(prefix, key), Some(b"asd".to_vec()));
assert!(storage.locks.lock().is_empty(), "Locks map should be empty!");
}
}
+5 -2
View File
@@ -17,7 +17,9 @@
//! Db-based backend utility structures and functions, used by both
//! full and light storages.
use std::{io, convert::TryInto, sync::Arc};
#[cfg(feature = "kvdb-rocksdb")]
use std::sync::Arc;
use std::{io, convert::TryInto};
use kvdb::{KeyValueDB, DBTransaction};
#[cfg(feature = "kvdb-rocksdb")]
@@ -32,11 +34,12 @@ use runtime_primitives::traits::{
Block as BlockT, Header as HeaderT, Zero, UniqueSaturatedFrom,
UniqueSaturatedInto, CheckedConversion
};
#[cfg(feature = "kvdb-rocksdb")]
use crate::DatabaseSettings;
/// Number of columns in the db. Must be the same for both full && light dbs.
/// Otherwise RocksDb will fail to open database && check its type.
pub const NUM_COLUMNS: u32 = 9;
pub const NUM_COLUMNS: u32 = 10;
/// Meta column. The set of keys in the column is shared by full && light storages.
pub const COLUMN_META: Option<u32> = Some(0);
+24
View File
@@ -139,6 +139,8 @@ pub trait Backend<Block, H>: AuxStore + Send + Sync where
type State: StateBackend<H>;
/// Changes trie storage.
type ChangesTrieStorage: PrunableStateChangesTrieStorage<Block, H>;
/// Offchain workers local storage.
type OffchainStorage: OffchainStorage;
/// Begin a new block insertion transaction with given parent block id.
/// When constructing the genesis, this is called with all-zero hash.
@@ -156,6 +158,8 @@ pub trait Backend<Block, H>: AuxStore + Send + Sync where
fn used_state_cache_size(&self) -> Option<usize>;
/// Returns reference to changes trie storage.
fn changes_trie_storage(&self) -> Option<&Self::ChangesTrieStorage>;
/// Returns a handle to offchain storage.
fn offchain_storage(&self) -> Option<Self::OffchainStorage>;
/// Returns true if state for given block is available.
fn have_state_at(&self, hash: &Block::Hash, _number: NumberFor<Block>) -> bool {
self.state_at(BlockId::Hash(hash.clone())).is_ok()
@@ -194,6 +198,26 @@ pub trait Backend<Block, H>: AuxStore + Send + Sync where
fn get_import_lock(&self) -> &Mutex<()>;
}
/// Offchain workers local storage.
pub trait OffchainStorage: Clone + Send + Sync {
/// Persist a value in storage under given key and prefix.
fn set(&mut self, prefix: &[u8], key: &[u8], value: &[u8]);
/// Retrieve a value from storage under given key and prefix.
fn get(&self, prefix: &[u8], key: &[u8]) -> Option<Vec<u8>>;
/// Replace the value in storage if given old_value matches the current one.
///
/// Returns `true` if the value has been set and false otherwise.
fn compare_and_set(
&mut self,
prefix: &[u8],
key: &[u8],
old_value: &[u8],
new_value: &[u8],
) -> bool;
}
/// Changes trie storage that supports pruning.
pub trait PrunableStateChangesTrieStorage<Block: BlockT, H: Hasher>:
StateChangesTrieStorage<H, NumberFor<Block>>
+61
View File
@@ -593,6 +593,7 @@ where
type Blockchain = Blockchain<Block>;
type State = InMemory<H>;
type ChangesTrieStorage = ChangesTrieStorage<Block, H>;
type OffchainStorage = OffchainStorage;
fn begin_operation(&self) -> error::Result<Self::BlockImportOperation> {
let old_state = self.state_at(BlockId::Hash(Default::default()))?;
@@ -669,6 +670,10 @@ where
Some(&self.changes_trie_storage)
}
fn offchain_storage(&self) -> Option<Self::OffchainStorage> {
None
}
fn state_at(&self, block: BlockId<Block>) -> error::Result<Self::State> {
match block {
BlockId::Hash(h) if h == Default::default() => {
@@ -768,8 +773,46 @@ pub fn check_genesis_storage(top: &StorageOverlay, children: &ChildrenStorageOve
Ok(())
}
/// In-memory storage for offchain workers.
#[derive(Debug, Clone, Default)]
pub struct OffchainStorage {
storage: HashMap<Vec<u8>, Vec<u8>>,
}
impl backend::OffchainStorage for OffchainStorage {
fn set(&mut self, prefix: &[u8], key: &[u8], value: &[u8]) {
let key = prefix.iter().chain(key).cloned().collect();
self.storage.insert(key, value.to_vec());
}
fn get(&self, prefix: &[u8], key: &[u8]) -> Option<Vec<u8>> {
let key: Vec<u8> = prefix.iter().chain(key).cloned().collect();
self.storage.get(&key).cloned()
}
fn compare_and_set(
&mut self,
prefix: &[u8],
key: &[u8],
old_value: &[u8],
new_value: &[u8],
) -> bool {
let key = prefix.iter().chain(key).cloned().collect();
let mut set = false;
self.storage.entry(key).and_modify(|val| {
if val.as_slice() == old_value {
*val = new_value.to_vec();
set = true;
}
});
set
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use test_client;
use primitives::Blake2Hasher;
@@ -789,4 +832,22 @@ mod tests {
test_client::trait_tests::test_blockchain_query_by_number_gets_canonical(backend);
}
#[test]
fn in_memory_offchain_storage() {
use crate::backend::OffchainStorage as _;
let mut storage = OffchainStorage::default();
assert_eq!(storage.get(b"A", b"B"), None);
assert_eq!(storage.get(b"B", b"A"), None);
storage.set(b"A", b"B", b"C");
assert_eq!(storage.get(b"A", b"B"), Some(b"C".to_vec()));
assert_eq!(storage.get(b"B", b"A"), None);
storage.compare_and_set(b"A", b"B", b"X", b"D");
assert_eq!(storage.get(b"A", b"B"), Some(b"C".to_vec()));
storage.compare_and_set(b"A", b"B", b"C", b"D");
assert_eq!(storage.get(b"A", b"B"), Some(b"D".to_vec()));
}
}
@@ -118,6 +118,7 @@ impl<S, F, Block, H> ClientBackend<Block, H> for Backend<S, F, H> where
type Blockchain = Blockchain<S, F>;
type State = OnDemandOrGenesisState<Block, S, F, H>;
type ChangesTrieStorage = in_mem::ChangesTrieStorage<Block, H>;
type OffchainStorage = in_mem::OffchainStorage;
fn begin_operation(&self) -> ClientResult<Self::BlockImportOperation> {
Ok(ImportOperation {
@@ -195,6 +196,10 @@ impl<S, F, Block, H> ClientBackend<Block, H> for Backend<S, F, H> where
None
}
fn offchain_storage(&self) -> Option<Self::OffchainStorage> {
None
}
fn state_at(&self, block: BlockId<Block>) -> ClientResult<Self::State> {
let block_number = self.blockchain.expect_block_number_from_id(&block)?;
+2
View File
@@ -26,6 +26,8 @@ tiny-keccak = "1.4.2"
assert_matches = "1.1"
wabt = "~0.7.4"
hex-literal = "0.2.0"
substrate-client = { path = "../client" }
substrate-offchain = { path = "../offchain/" }
[features]
default = []
+75 -4
View File
@@ -857,24 +857,28 @@ impl_function_executor!(this: FunctionExecutor<'e, E>,
.map_err(|_| "Invalid attempt to set value in ext_random_seed")?;
Ok(())
},
ext_local_storage_set(key: *const u8, key_len: u32, value: *const u8, value_len: u32) => {
ext_local_storage_set(kind: u32, key: *const u8, key_len: u32, value: *const u8, value_len: u32) => {
let kind = offchain::StorageKind::try_from(kind)
.map_err(|_| "storage kind OOB while ext_local_storage_set: wasm")?;
let key = this.memory.get(key, key_len as usize)
.map_err(|_| "OOB while ext_local_storage_set: wasm")?;
let value = this.memory.get(value, value_len as usize)
.map_err(|_| "OOB while ext_local_storage_set: wasm")?;
this.ext.offchain()
.map(|api| api.local_storage_set(&key, &value))
.map(|api| api.local_storage_set(kind, &key, &value))
.ok_or_else(|| "Calling unavailable API ext_local_storage_set: wasm")?;
Ok(())
},
ext_local_storage_get(key: *const u8, key_len: u32, value_len: *mut u32) -> *mut u8 => {
ext_local_storage_get(kind: u32, key: *const u8, key_len: u32, value_len: *mut u32) -> *mut u8 => {
let kind = offchain::StorageKind::try_from(kind)
.map_err(|_| "storage kind OOB while ext_local_storage_get: wasm")?;
let key = this.memory.get(key, key_len as usize)
.map_err(|_| "OOB while ext_local_storage_get: wasm")?;
let maybe_value = this.ext.offchain()
.map(|api| api.local_storage_get(&key))
.map(|api| api.local_storage_get(kind, &key))
.ok_or_else(|| "Calling unavailable API ext_local_storage_get: wasm")?;
let (offset, len) = if let Some(value) = maybe_value {
@@ -891,6 +895,31 @@ impl_function_executor!(this: FunctionExecutor<'e, E>,
Ok(offset)
},
ext_local_storage_compare_and_set(
kind: u32,
key: *const u8,
key_len: u32,
old_value: *const u8,
old_value_len: u32,
new_value: *const u8,
new_value_len: u32
) -> u32 => {
let kind = offchain::StorageKind::try_from(kind)
.map_err(|_| "storage kind OOB while ext_local_storage_compare_and_set: wasm")?;
let key = this.memory.get(key, key_len as usize)
.map_err(|_| "OOB while ext_local_storage_compare_and_set: wasm")?;
let old_value = this.memory.get(old_value, old_value_len as usize)
.map_err(|_| "OOB while ext_local_storage_compare_and_set: wasm")?;
let new_value = this.memory.get(new_value, new_value_len as usize)
.map_err(|_| "OOB while ext_local_storage_compare_and_set: wasm")?;
let res = this.ext.offchain()
.map(|api| api.local_storage_compare_and_set(kind, &key, &old_value, &new_value))
.ok_or_else(|| "Calling unavailable API ext_local_storage_compare_andset: wasm")?;
Ok(if res { 0 } else { 1 })
},
ext_http_request_start(
method: *const u8,
method_len: u32,
@@ -1365,6 +1394,7 @@ mod tests {
use state_machine::TestExternalities as CoreTestExternalities;
use hex_literal::hex;
use primitives::map;
use substrate_offchain::testing;
type TestExternalities<H> = CoreTestExternalities<H, u64>;
@@ -1550,4 +1580,45 @@ mod tests {
ordered_trie_root::<Blake2Hasher, _, _>(vec![b"zero".to_vec(), b"one".to_vec(), b"two".to_vec()].iter()).as_fixed_bytes().encode()
);
}
#[test]
fn offchain_local_storage_should_work() {
use substrate_client::backend::OffchainStorage;
let mut ext = TestExternalities::<Blake2Hasher>::default();
let (offchain, state) = testing::TestOffchainExt::new();
ext.set_offchain_externalities(offchain);
let test_code = include_bytes!("../wasm/target/wasm32-unknown-unknown/release/runtime_test.compact.wasm");
assert_eq!(
WasmExecutor::new().call(&mut ext, 8, &test_code[..], "test_offchain_local_storage", &[]).unwrap(),
vec![0]
);
assert_eq!(state.read().persistent_storage.get(b"", b"test"), Some(vec![]));
}
#[test]
fn offchain_http_should_work() {
let mut ext = TestExternalities::<Blake2Hasher>::default();
let (offchain, state) = testing::TestOffchainExt::new();
ext.set_offchain_externalities(offchain);
state.write().expect_request(
0,
testing::PendingRequest {
method: "POST".into(),
uri: "http://localhost:12345".into(),
body: vec![1, 2, 3, 4],
headers: vec![("X-Auth".to_owned(), "test".to_owned())],
sent: true,
response: vec![1, 2, 3],
response_headers: vec![("X-Auth".to_owned(), "hello".to_owned())],
..Default::default()
},
);
let test_code = include_bytes!("../wasm/target/wasm32-unknown-unknown/release/runtime_test.compact.wasm");
assert_eq!(
WasmExecutor::new().call(&mut ext, 8, &test_code[..], "test_offchain_http", &[]).unwrap(),
vec![0]
);
}
}
+1
View File
@@ -125,6 +125,7 @@ version = "2.0.0"
dependencies = [
"sr-io 2.0.0",
"sr-sandbox 2.0.0",
"sr-std 2.0.0",
"substrate-primitives 2.0.0",
]
+1
View File
@@ -8,6 +8,7 @@ edition = "2018"
crate-type = ["cdylib"]
[dependencies]
rstd = { package = "sr-std", path = "../../sr-std", default-features = false }
runtime_io = { package = "sr-io", path = "../../sr-io", default-features = false }
sandbox = { package = "sr-sandbox", path = "../../sr-sandbox", default-features = false }
substrate-primitives = { path = "../../primitives", default-features = false }
+38 -5
View File
@@ -1,9 +1,7 @@
#![no_std]
#![cfg_attr(feature = "strict", deny(warnings))]
extern crate alloc;
use alloc::vec::Vec;
use alloc::slice;
use rstd::{slice, vec::Vec, vec};
use runtime_io::{
set_storage, storage, clear_prefix, print, blake2_128, blake2_256,
@@ -11,7 +9,7 @@ use runtime_io::{
};
macro_rules! impl_stubs {
( $( $new_name:ident => $invoke:expr ),* ) => {
( $( $new_name:ident => $invoke:expr, )* ) => {
$(
impl_stubs!(@METHOD $new_name => $invoke);
)*
@@ -134,7 +132,42 @@ impl_stubs!(
Err(sandbox::Error::OutOfBounds) => 3,
};
[code].to_vec()
}
},
test_offchain_local_storage => |_| {
let kind = substrate_primitives::offchain::StorageKind::PERSISTENT;
assert_eq!(runtime_io::local_storage_get(kind, b"test"), None);
runtime_io::local_storage_set(kind, b"test", b"asd");
assert_eq!(runtime_io::local_storage_get(kind, b"test"), Some(b"asd".to_vec()));
let res = runtime_io::local_storage_compare_and_set(kind, b"test", b"asd", b"");
assert_eq!(res, true);
assert_eq!(runtime_io::local_storage_get(kind, b"test"), Some(b"".to_vec()));
[0].to_vec()
},
test_offchain_http => |_| {
use substrate_primitives::offchain::HttpRequestStatus;
let run = || -> Option<()> {
let id = runtime_io::http_request_start("POST", "http://localhost:12345", &[]).ok()?;
runtime_io::http_request_add_header(id, "X-Auth", "test").ok()?;
runtime_io::http_request_write_body(id, &[1, 2, 3, 4], None).ok()?;
runtime_io::http_request_write_body(id, &[], None).ok()?;
let status = runtime_io::http_response_wait(&[id], None);
assert!(status == vec![HttpRequestStatus::Finished(200)], "Expected Finished(200) status.");
let headers = runtime_io::http_response_headers(id);
assert_eq!(headers, vec![(b"X-Auth".to_vec(), b"hello".to_vec())]);
let mut buffer = vec![0; 64];
let read = runtime_io::http_response_read_body(id, &mut buffer, None).ok()?;
assert_eq!(read, 3);
assert_eq!(&buffer[0..read], &[1, 2, 3]);
let read = runtime_io::http_response_read_body(id, &mut buffer, None).ok()?;
assert_eq!(read, 0);
Some(())
};
[if run().is_some() { 0 } else { 1 }].to_vec()
},
);
fn execute_sandboxed(code: &[u8], args: &[sandbox::TypedValue]) -> Result<sandbox::ReturnValue, sandbox::HostError> {
+1 -1
View File
@@ -8,7 +8,6 @@ edition = "2018"
[dependencies]
client = { package = "substrate-client", path = "../../core/client" }
consensus = { package = "substrate-consensus-common", path = "../../core/consensus/common" }
futures = "0.1.25"
log = "0.4"
offchain-primitives = { package = "substrate-offchain-primitives", path = "./primitives" }
@@ -20,6 +19,7 @@ transaction_pool = { package = "substrate-transaction-pool", path = "../../core/
[dev-dependencies]
env_logger = "0.6"
client-db = { package = "substrate-client-db", path = "../../core/client/db/", default-features = true }
test-client = { package = "substrate-test-runtime-client", path = "../../core/test-runtime/client" }
tokio = "0.1.7"
+103 -17
View File
@@ -15,6 +15,7 @@
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
use std::sync::Arc;
use client::backend::OffchainStorage;
use futures::{Stream, Future, sync::mpsc};
use log::{info, debug, warn, error};
use parity_codec::Decode;
@@ -22,6 +23,7 @@ use primitives::offchain::{
Timestamp, HttpRequestId, HttpRequestStatus, HttpError,
Externalities as OffchainExt,
CryptoKind, CryptoKeyId,
StorageKind,
};
use runtime_primitives::{
generic::BlockId,
@@ -37,17 +39,24 @@ enum ExtMessage {
/// Asynchronous offchain API.
///
/// NOTE this is done to prevent recursive calls into the runtime (which are not supported currently).
pub(crate) struct AsyncApi(mpsc::UnboundedSender<ExtMessage>);
pub(crate) struct Api<S> {
sender: mpsc::UnboundedSender<ExtMessage>,
db: S,
}
fn unavailable_yet<R: Default>(name: &str) -> R {
error!("This {:?} API is not available for offchain workers yet. Follow
error!("The {:?} API is not available for offchain workers yet. Follow \
https://github.com/paritytech/substrate/issues/1458 for details", name);
Default::default()
}
impl OffchainExt for AsyncApi {
const LOCAL_DB: &str = "LOCAL (fork-aware) DB";
const STORAGE_PREFIX: &[u8] = b"storage";
impl<S: OffchainStorage> OffchainExt for Api<S> {
fn submit_transaction(&mut self, ext: Vec<u8>) -> Result<(), ()> {
self.0.unbounded_send(ExtMessage::SubmitExtrinsic(ext))
self.sender
.unbounded_send(ExtMessage::SubmitExtrinsic(ext))
.map(|_| ())
.map_err(|_| ())
}
@@ -89,16 +98,33 @@ impl OffchainExt for AsyncApi {
unavailable_yet("random_seed")
}
fn local_storage_set(&mut self, _key: &[u8], _value: &[u8]) {
unavailable_yet("local_storage_set")
fn local_storage_set(&mut self, kind: StorageKind, key: &[u8], value: &[u8]) {
match kind {
StorageKind::PERSISTENT => self.db.set(STORAGE_PREFIX, key, value),
StorageKind::LOCAL => unavailable_yet(LOCAL_DB),
}
}
fn local_storage_compare_and_set(&mut self, _key: &[u8], _old_value: &[u8], _new_value: &[u8]) {
unavailable_yet("local_storage_compare_and_set")
fn local_storage_compare_and_set(
&mut self,
kind: StorageKind,
key: &[u8],
old_value: &[u8],
new_value: &[u8],
) -> bool {
match kind {
StorageKind::PERSISTENT => {
self.db.compare_and_set(STORAGE_PREFIX, key, old_value, new_value)
},
StorageKind::LOCAL => unavailable_yet(LOCAL_DB),
}
}
fn local_storage_get(&mut self, _key: &[u8]) -> Option<Vec<u8>> {
unavailable_yet("local_storage_get")
fn local_storage_get(&mut self, kind: StorageKind, key: &[u8]) -> Option<Vec<u8>> {
match kind {
StorageKind::PERSISTENT => self.db.get(STORAGE_PREFIX, key),
StorageKind::LOCAL => unavailable_yet(LOCAL_DB),
}
}
fn http_request_start(
@@ -159,24 +185,35 @@ impl OffchainExt for AsyncApi {
}
/// Offchain extensions implementation API
pub(crate) struct Api<A: ChainApi> {
///
/// This is the asynchronous processing part of the API.
pub(crate) struct AsyncApi<A: ChainApi> {
receiver: Option<mpsc::UnboundedReceiver<ExtMessage>>,
transaction_pool: Arc<Pool<A>>,
at: BlockId<A::Block>,
}
impl<A: ChainApi> Api<A> {
pub fn new(
impl<A: ChainApi> AsyncApi<A> {
/// Creates new Offchain extensions API implementation an the asynchronous processing part.
pub fn new<S: OffchainStorage>(
transaction_pool: Arc<Pool<A>>,
db: S,
at: BlockId<A::Block>,
) -> (AsyncApi, Self) {
let (tx, rx) = mpsc::unbounded();
let api = Self {
) -> (Api<S>, AsyncApi<A>) {
let (sender, rx) = mpsc::unbounded();
let api = Api {
sender,
db,
};
let async_api = AsyncApi {
receiver: Some(rx),
transaction_pool,
at,
};
(AsyncApi(tx), api)
(api, async_api)
}
/// Run a processing task for the API
@@ -209,3 +246,52 @@ impl<A: ChainApi> Api<A> {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use client_db::offchain::LocalStorage;
fn offchain_api() -> (Api<LocalStorage>, AsyncApi<impl ChainApi>) {
let _ = env_logger::try_init();
let db = LocalStorage::new_test();
let client = Arc::new(test_client::new());
let pool = Arc::new(
Pool::new(Default::default(), transaction_pool::ChainApi::new(client.clone()))
);
AsyncApi::new(pool, db, BlockId::Number(0))
}
#[test]
fn should_set_and_get_local_storage() {
// given
let kind = StorageKind::PERSISTENT;
let mut api = offchain_api().0;
let key = b"test";
// when
assert_eq!(api.local_storage_get(kind, key), None);
api.local_storage_set(kind, key, b"value");
// then
assert_eq!(api.local_storage_get(kind, key), Some(b"value".to_vec()));
}
#[test]
fn should_compare_and_set_local_storage() {
// given
let kind = StorageKind::PERSISTENT;
let mut api = offchain_api().0;
let key = b"test";
api.local_storage_set(kind, key, b"value");
// when
assert_eq!(api.local_storage_compare_and_set(kind, key, b"val", b"xxx"), false);
assert_eq!(api.local_storage_get(kind, key), Some(b"value".to_vec()));
// when
assert_eq!(api.local_storage_compare_and_set(kind, key, b"value", b"xxx"), true);
assert_eq!(api.local_storage_get(kind, key), Some(b"xxx".to_vec()));
}
}
+20 -11
View File
@@ -56,31 +56,35 @@ pub mod testing;
pub use offchain_primitives::OffchainWorkerApi;
/// An offchain workers manager.
pub struct OffchainWorkers<C, Block: traits::Block> {
pub struct OffchainWorkers<C, S, Block: traits::Block> {
client: Arc<C>,
db: S,
_block: PhantomData<Block>,
}
impl<C, Block: traits::Block> fmt::Debug for OffchainWorkers<C, Block> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_tuple("OffchainWorkers").finish()
}
}
impl<C, Block: traits::Block> OffchainWorkers<C, Block> {
impl<C, S, Block: traits::Block> OffchainWorkers<C, S, Block> {
/// Creates new `OffchainWorkers`.
pub fn new(
client: Arc<C>,
db: S,
) -> Self {
Self {
client,
db,
_block: PhantomData,
}
}
}
impl<C, Block> OffchainWorkers<C, Block> where
impl<C, S, Block: traits::Block> fmt::Debug for OffchainWorkers<C, S, Block> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_tuple("OffchainWorkers").finish()
}
}
impl<C, S, Block> OffchainWorkers<C, S, Block> where
Block: traits::Block,
S: client::backend::OffchainStorage + 'static,
C: ProvideRuntimeApi,
C::Api: OffchainWorkerApi<Block>,
{
@@ -99,7 +103,11 @@ impl<C, Block> OffchainWorkers<C, Block> where
debug!("Checking offchain workers at {:?}: {:?}", at, has_api);
if has_api.unwrap_or(false) {
let (api, runner) = api::Api::new(pool.clone(), at.clone());
let (api, runner) = api::AsyncApi::new(
pool.clone(),
self.db.clone(),
at.clone(),
);
debug!("Running offchain workers at {:?}", at);
let api = Box::new(api);
runtime.offchain_worker_with_context(&at, ExecutionContext::OffchainWorker(api), *number).unwrap();
@@ -122,9 +130,10 @@ mod tests {
let runtime = tokio::runtime::Runtime::new().unwrap();
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();
// when
let offchain = OffchainWorkers::new(client);
let offchain = OffchainWorkers::new(client, db);
runtime.executor().spawn(offchain.on_block_imported(&0u64, &pool));
// then
+76 -15
View File
@@ -20,6 +20,7 @@ use std::{
collections::BTreeMap,
sync::Arc,
};
use client::backend::OffchainStorage;
use parking_lot::RwLock;
use primitives::offchain::{
self,
@@ -29,6 +30,7 @@ use primitives::offchain::{
Timestamp,
CryptoKind,
CryptoKeyId,
StorageKind,
};
/// Pending request.
@@ -61,6 +63,11 @@ pub struct PendingRequest {
pub struct State {
/// A list of pending requests.
pub requests: BTreeMap<RequestId, PendingRequest>,
expected_requests: BTreeMap<RequestId, PendingRequest>,
/// Persistent local storage
pub persistent_storage: client::in_mem::OffchainStorage,
/// Local storage
pub local_storage: client::in_mem::OffchainStorage,
}
impl State {
@@ -74,7 +81,7 @@ impl State {
) {
match self.requests.get_mut(&RequestId(id)) {
None => {
panic!("Missing expected request: {:?}.\n\nAll: {:?}", id, self.requests);
panic!("Missing pending request: {:?}.\n\nAll: {:?}", id, self.requests);
}
Some(req) => {
assert_eq!(
@@ -86,12 +93,47 @@ impl State {
}
}
}
fn fulfill_expected(&mut self, id: u16) {
if let Some(mut req) = self.expected_requests.remove(&RequestId(id)) {
let response = std::mem::replace(&mut req.response, vec![]);
let headers = std::mem::replace(&mut req.response_headers, vec![]);
self.fulfill_pending_request(id, req, response, headers);
}
}
/// Add expected HTTP request.
///
/// This method can be used to initialize expected HTTP requests and their responses
/// before running the actual code that utilizes them (for instance before calling into runtime).
/// Expected request has to be fulfilled before this struct is dropped,
/// the `response` and `response_headers` fields will be used to return results to the callers.
pub fn expect_request(&mut self, id: u16, expected: PendingRequest) {
self.expected_requests.insert(RequestId(id), expected);
}
}
impl Drop for State {
fn drop(&mut self) {
if !self.expected_requests.is_empty() {
panic!("Unfulfilled expected requests: {:?}", self.expected_requests);
}
}
}
/// Implementation of offchain externalities used for tests.
#[derive(Clone, Default, Debug)]
pub struct TestOffchainExt(pub Arc<RwLock<State>>);
impl TestOffchainExt {
/// Create new `TestOffchainExt` and a reference to the internal state.
pub fn new() -> (Self, Arc<RwLock<State>>) {
let ext = Self::default();
let state = ext.0.clone();
(ext, state)
}
}
impl offchain::Externalities for TestOffchainExt {
fn submit_transaction(&mut self, _ex: Vec<u8>) -> Result<(), ()> {
unimplemented!("not needed in tests so far")
@@ -129,21 +171,34 @@ impl offchain::Externalities for TestOffchainExt {
unimplemented!("not needed in tests so far")
}
fn local_storage_set(&mut self, _key: &[u8], _value: &[u8]) {
unimplemented!("not needed in tests so far")
fn local_storage_set(&mut self, kind: StorageKind, key: &[u8], value: &[u8]) {
let mut state = self.0.write();
match kind {
StorageKind::LOCAL => &mut state.local_storage,
StorageKind::PERSISTENT => &mut state.persistent_storage,
}.set(b"", key, value);
}
fn local_storage_compare_and_set(
&mut self,
_key: &[u8],
_old_value: &[u8],
_new_value: &[u8]
) {
unimplemented!("not needed in tests so far")
kind: StorageKind,
key: &[u8],
old_value: &[u8],
new_value: &[u8]
) -> bool {
let mut state = self.0.write();
match kind {
StorageKind::LOCAL => &mut state.local_storage,
StorageKind::PERSISTENT => &mut state.persistent_storage,
}.compare_and_set(b"", key, old_value, new_value)
}
fn local_storage_get(&mut self, _key: &[u8]) -> Option<Vec<u8>> {
unimplemented!("not needed in tests so far")
fn local_storage_get(&mut self, kind: StorageKind, key: &[u8]) -> Option<Vec<u8>> {
let state = self.0.read();
match kind {
StorageKind::LOCAL => &state.local_storage,
StorageKind::PERSISTENT => &state.persistent_storage,
}.get(b"", key)
}
fn http_request_start(&mut self, method: &str, uri: &str, meta: &[u8]) -> Result<RequestId, ()> {
@@ -180,15 +235,21 @@ impl offchain::Externalities for TestOffchainExt {
_deadline: Option<Timestamp>
) -> Result<(), HttpError> {
let mut state = self.0.write();
if let Some(req) = state.requests.get_mut(&request_id) {
let sent = {
let req = state.requests.get_mut(&request_id).ok_or(HttpError::IoError)?;
req.body.extend(chunk);
if chunk.is_empty() {
req.sent = true;
}
req.body.extend(chunk);
Ok(())
} else {
Err(HttpError::IoError)
req.sent
};
if sent {
state.fulfill_expected(request_id.0);
}
Ok(())
}
fn http_response_wait(
+55 -10
View File
@@ -19,6 +19,37 @@
use rstd::prelude::{Vec, Box};
use rstd::convert::TryFrom;
/// A type of supported crypto.
#[derive(Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "std", derive(Debug))]
#[repr(C)]
pub enum StorageKind {
/// Persistent storage is non-revertible and not fork-aware. It means that any value
/// set by the offchain worker triggered at block `N(hash1)` is persisted even
/// if that block is reverted as non-canonical and is available for the worker
/// that is re-run at block `N(hash2)`.
/// This storage can be used by offchain workers to handle forks
/// and coordinate offchain workers running on different forks.
PERSISTENT = 1,
/// Local storage is revertible and fork-aware. It means that any value
/// set by the offchain worker triggered at block `N(hash1)` is reverted
/// if that block is reverted as non-canonical and is NOT available for the worker
/// that is re-run at block `N(hash2)`.
LOCAL = 2,
}
impl TryFrom<u32> for StorageKind {
type Error = ();
fn try_from(kind: u32) -> Result<Self, Self::Error> {
match kind {
e if e == u32::from(StorageKind::PERSISTENT as u8) => Ok(StorageKind::PERSISTENT),
e if e == u32::from(StorageKind::LOCAL as u8) => Ok(StorageKind::LOCAL),
_ => Err(()),
}
}
}
/// A type of supported crypto.
#[derive(Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "std", derive(Debug))]
@@ -37,7 +68,7 @@ impl TryFrom<u32> for CryptoKind {
match kind {
e if e == u32::from(CryptoKind::Sr25519 as u8) => Ok(CryptoKind::Sr25519),
e if e == u32::from(CryptoKind::Ed25519 as u8) => Ok(CryptoKind::Ed25519),
_ => Err(())
_ => Err(()),
}
}
}
@@ -227,23 +258,31 @@ pub trait Externalities {
///
/// Note this storage is not part of the consensus, it's only accessible by
/// offchain worker tasks running on the same machine. It IS persisted between runs.
fn local_storage_set(&mut self, key: &[u8], value: &[u8]);
fn local_storage_set(&mut self, kind: StorageKind, key: &[u8], value: &[u8]);
/// Sets a value in the local storage if it matches current value.
///
/// Since multiple offchain workers may be running concurrently, to prevent
/// data races use CAS to coordinate between them.
///
/// Returns `true` if the value has been set, `false` otherwise.
///
/// Note this storage is not part of the consensus, it's only accessible by
/// offchain worker tasks running on the same machine. It IS persisted between runs.
fn local_storage_compare_and_set(&mut self, key: &[u8], old_value: &[u8], new_value: &[u8]);
fn local_storage_compare_and_set(
&mut self,
kind: StorageKind,
key: &[u8],
old_value: &[u8],
new_value: &[u8],
) -> bool;
/// Gets a value from the local storage.
///
/// If the value does not exist in the storage `None` will be returned.
/// Note this storage is not part of the consensus, it's only accessible by
/// offchain worker tasks running on the same machine. It IS persisted between runs.
fn local_storage_get(&mut self, key: &[u8]) -> Option<Vec<u8>>;
fn local_storage_get(&mut self, kind: StorageKind, key: &[u8]) -> Option<Vec<u8>>;
/// Initiaties a http request given HTTP verb and the URL.
///
@@ -348,16 +387,22 @@ impl<T: Externalities + ?Sized> Externalities for Box<T> {
(&mut **self).random_seed()
}
fn local_storage_set(&mut self, key: &[u8], value: &[u8]) {
(&mut **self).local_storage_set(key, value)
fn local_storage_set(&mut self, kind: StorageKind, key: &[u8], value: &[u8]) {
(&mut **self).local_storage_set(kind, key, value)
}
fn local_storage_compare_and_set(&mut self, key: &[u8], old_value: &[u8], new_value: &[u8]) {
(&mut **self).local_storage_compare_and_set(key, old_value, new_value)
fn local_storage_compare_and_set(
&mut self,
kind: StorageKind,
key: &[u8],
old_value: &[u8],
new_value: &[u8],
) -> bool {
(&mut **self).local_storage_compare_and_set(kind, key, old_value, new_value)
}
fn local_storage_get(&mut self, key: &[u8]) -> Option<Vec<u8>> {
(&mut **self).local_storage_get(key)
fn local_storage_get(&mut self, kind: StorageKind, key: &[u8]) -> Option<Vec<u8>> {
(&mut **self).local_storage_get(kind, key)
}
fn http_request_start(&mut self, method: &str, uri: &str, meta: &[u8]) -> Result<HttpRequestId, ()> {
+15 -2
View File
@@ -121,6 +121,11 @@ pub type ComponentClient<C> = Client<
<C as Components>::RuntimeApi,
>;
/// A offchain workers storage backend type.
pub type ComponentOffchainStorage<C> = <
<C as Components>::Backend as client::backend::Backend<ComponentBlock<C>, Blake2Hasher>
>::OffchainStorage;
/// Block type for `Components`
pub type ComponentBlock<C> = <<C as Components>::Factory as ServiceFactory>::Block;
@@ -259,7 +264,11 @@ impl<C: Components> MaintainTransactionPool<Self> for C where
pub trait OffchainWorker<C: Components> {
fn offchain_workers(
number: &FactoryBlockNumber<C::Factory>,
offchain: &offchain::OffchainWorkers<ComponentClient<C>, ComponentBlock<C>>,
offchain: &offchain::OffchainWorkers<
ComponentClient<C>,
ComponentOffchainStorage<C>,
ComponentBlock<C>
>,
pool: &Arc<TransactionPool<C::TransactionPoolApi>>,
) -> error::Result<Box<dyn Future<Item = (), Error = ()> + Send>>;
}
@@ -270,7 +279,11 @@ impl<C: Components> OffchainWorker<Self> for C where
{
fn offchain_workers(
number: &FactoryBlockNumber<C::Factory>,
offchain: &offchain::OffchainWorkers<ComponentClient<C>, ComponentBlock<C>>,
offchain: &offchain::OffchainWorkers<
ComponentClient<C>,
ComponentOffchainStorage<C>,
ComponentBlock<C>
>,
pool: &Arc<TransactionPool<C::TransactionPoolApi>>,
) -> error::Result<Box<dyn Future<Item = (), Error = ()> + Send>> {
Ok(Box::new(offchain.on_block_imported(number, pool)))
+21 -8
View File
@@ -32,7 +32,7 @@ use std::time::Duration;
use futures::sync::mpsc;
use parking_lot::Mutex;
use client::{BlockchainEvents, backend::Backend};
use client::{BlockchainEvents, backend::Backend, runtime_api::BlockT};
use exit_future::Signal;
use futures::prelude::*;
use keystore::Store as Keystore;
@@ -51,11 +51,10 @@ pub use chain_spec::{ChainSpec, Properties};
pub use transaction_pool::txpool::{
self, Pool as TransactionPool, Options as TransactionPoolOptions, ChainApi, IntoPoolError
};
use client::runtime_api::BlockT;
pub use client::FinalityNotifications;
pub use components::{ServiceFactory, FullBackend, FullExecutor, LightBackend,
LightExecutor, Components, PoolApi, ComponentClient,
LightExecutor, Components, PoolApi, ComponentClient, ComponentOffchainStorage,
ComponentBlock, FullClient, LightClient, FullComponents, LightComponents,
CodeExecutor, NetworkService, FactoryChainSpec, FactoryBlock,
FactoryFullConfiguration, RuntimeGenesis, FactoryGenesis,
@@ -94,8 +93,12 @@ pub struct Service<Components: components::Components> {
pub config: FactoryFullConfiguration<Components::Factory>,
_rpc: Box<dyn std::any::Any + Send + Sync>,
_telemetry: Option<tel::Telemetry>,
_offchain_workers: Option<Arc<offchain::OffchainWorkers<ComponentClient<Components>, ComponentBlock<Components>>>>,
_telemetry_on_connect_sinks: Arc<Mutex<Vec<mpsc::UnboundedSender<()>>>>,
_offchain_workers: Option<Arc<offchain::OffchainWorkers<
ComponentClient<Components>,
ComponentOffchainStorage<Components>,
ComponentBlock<Components>>
>>,
}
/// Creates bare client without any networking.
@@ -234,10 +237,20 @@ impl<Components: components::Components> Service<Components> {
.select(exit.clone())
.then(|_| Ok(()))));
let offchain_workers = if config.offchain_worker {
Some(Arc::new(offchain::OffchainWorkers::new(client.clone())))
} else {
None
#[allow(deprecated)]
let offchain_storage = client.backend().offchain_storage();
let offchain_workers = match (config.offchain_worker, offchain_storage) {
(true, Some(db)) => {
Some(Arc::new(offchain::OffchainWorkers::new(
client.clone(),
db,
)))
},
(true, None) => {
log::warn!("Offchain workers disabled, due to lack of offchain storage support in backend.");
None
},
_ => None,
};
{
+11 -4
View File
@@ -33,7 +33,12 @@ use rstd::vec::Vec;
pub use codec;
pub use primitives::Blake2Hasher;
use primitives::offchain::{Timestamp, HttpRequestId, HttpRequestStatus, HttpError, CryptoKind, CryptoKeyId};
use primitives::offchain::{
Timestamp,
HttpRequestId, HttpRequestStatus, HttpError,
CryptoKind, CryptoKeyId,
StorageKind,
};
/// Error verifying ECDSA signature
pub enum EcdsaVerifyError {
@@ -283,23 +288,25 @@ export_api! {
///
/// Note this storage is not part of the consensus, it's only accessible by
/// offchain worker tasks running on the same machine. It IS persisted between runs.
fn local_storage_set(key: &[u8], value: &[u8]);
fn local_storage_set(kind: StorageKind, key: &[u8], value: &[u8]);
/// Sets a value in the local storage if it matches current value.
///
/// Since multiple offchain workers may be running concurrently, to prevent
/// data races use CAS to coordinate between them.
///
/// Returns `true` if the value has been set, `false` otherwise.
///
/// Note this storage is not part of the consensus, it's only accessible by
/// offchain worker tasks running on the same machine. It IS persisted between runs.
fn local_storage_compare_and_set(key: &[u8], old_value: &[u8], new_value: &[u8]);
fn local_storage_compare_and_set(kind: StorageKind, key: &[u8], old_value: &[u8], new_value: &[u8]) -> bool;
/// Gets a value from the local storage.
///
/// If the value does not exist in the storage `None` will be returned.
/// Note this storage is not part of the consensus, it's only accessible by
/// offchain worker tasks running on the same machine. It IS persisted between runs.
fn local_storage_get(key: &[u8]) -> Option<Vec<u8>>;
fn local_storage_get(kind: StorageKind, key: &[u8]) -> Option<Vec<u8>>;
/// Initiaties a http request given HTTP verb and the URL.
///
+2 -4
View File
@@ -486,9 +486,8 @@ mod tests {
#[test]
fn should_send_a_basic_request_and_get_response() {
let offchain = testing::TestOffchainExt::default();
let (offchain, state) = testing::TestOffchainExt::new();
let mut t = TestExternalities::default();
let state = offchain.0.clone();
t.set_offchain_externalities(offchain);
with_externalities(&mut t, || {
@@ -528,9 +527,8 @@ mod tests {
#[test]
fn should_send_a_post_request() {
let offchain = testing::TestOffchainExt::default();
let (offchain, state) = testing::TestOffchainExt::new();
let mut t = TestExternalities::default();
let state = offchain.0.clone();
t.set_offchain_externalities(offchain);
with_externalities(&mut t, || {
+12 -7
View File
@@ -305,7 +305,7 @@ impl OffchainApi for () {
}, "timestamp can be called only in the offchain worker context")
}
fn sleep_until(deadline: Timestamp) {
fn sleep_until(deadline: offchain::Timestamp) {
with_offchain(|ext| {
ext.sleep_until(deadline)
}, "sleep_until can be called only in the offchain worker context")
@@ -317,21 +317,26 @@ impl OffchainApi for () {
}, "random_seed can be called only in the offchain worker context")
}
fn local_storage_set(key: &[u8], value: &[u8]) {
fn local_storage_set(kind: offchain::StorageKind, key: &[u8], value: &[u8]) {
with_offchain(|ext| {
ext.local_storage_set(key, value)
ext.local_storage_set(kind, key, value)
}, "local_storage_set can be called only in the offchain worker context")
}
fn local_storage_compare_and_set(key: &[u8], old_value: &[u8], new_value: &[u8]) {
fn local_storage_compare_and_set(
kind: offchain::StorageKind,
key: &[u8],
old_value: &[u8],
new_value: &[u8],
) -> bool {
with_offchain(|ext| {
ext.local_storage_compare_and_set(key, old_value, new_value)
ext.local_storage_compare_and_set(kind, key, old_value, new_value)
}, "local_storage_compare_and_set can be called only in the offchain worker context")
}
fn local_storage_get(key: &[u8]) -> Option<Vec<u8>> {
fn local_storage_get(kind: offchain::StorageKind, key: &[u8]) -> Option<Vec<u8>> {
with_offchain(|ext| {
ext.local_storage_get(key)
ext.local_storage_get(kind, key)
}, "local_storage_get can be called only in the offchain worker context")
}
+16 -8
View File
@@ -459,17 +459,22 @@ pub mod ext {
fn ext_random_seed(data: *mut u8);
/// Write a value to local storage.
fn ext_local_storage_set(key: *const u8, key_len: u32, value: *const u8, value_len: u32);
fn ext_local_storage_set(kind: u32, key: *const u8, key_len: u32, value: *const u8, value_len: u32);
/// Write a value to local storage in atomic fashion.
///
/// # Returns
/// - `0` in case the value has been set
/// - `1` if the `old_value` didn't match
fn ext_local_storage_compare_and_set(
kind: u32,
key: *const u8,
key_len: u32,
old_value: *const u8,
old_value_len: u32,
new_value: *const u8,
new_value_len: u32
);
) -> u32;
/// Read a value from local storage.
///
@@ -478,7 +483,7 @@ pub mod ext {
///
/// - 0 if the value has not been found, the `value_len` is set to `u32::max_value`.
/// - Otherwise, pointer to the value in memory. `value_len` contains the length of the value.
fn ext_local_storage_get(key: *const u8, key_len: u32, value_len: *mut u32) -> *mut u8;
fn ext_local_storage_get(kind: u32, key: *const u8, key_len: u32, value_len: *mut u32) -> *mut u8;
/// Initiaties a http request.
///
@@ -932,7 +937,7 @@ impl OffchainApi for () {
})
}
fn sleep_until(deadline: Timestamp) {
fn sleep_until(deadline: offchain::Timestamp) {
unsafe {
ext_sleep_until.get()(deadline.unix_millis())
}
@@ -946,9 +951,10 @@ impl OffchainApi for () {
result
}
fn local_storage_set(key: &[u8], value: &[u8]) {
fn local_storage_set(kind: offchain::StorageKind, key: &[u8], value: &[u8]) {
unsafe {
ext_local_storage_set.get()(
kind as u8 as u32,
key.as_ptr(),
key.len() as u32,
value.as_ptr(),
@@ -957,23 +963,25 @@ impl OffchainApi for () {
}
}
fn local_storage_compare_and_set(key: &[u8], old_value: &[u8], new_value: &[u8]) {
fn local_storage_compare_and_set(kind: offchain::StorageKind, key: &[u8], old_value: &[u8], new_value: &[u8]) -> bool {
unsafe {
ext_local_storage_compare_and_set.get()(
kind as u8 as u32,
key.as_ptr(),
key.len() as u32,
old_value.as_ptr(),
old_value.len() as u32,
new_value.as_ptr(),
new_value.len() as u32,
)
) == 0
}
}
fn local_storage_get(key: &[u8]) -> Option<Vec<u8>> {
fn local_storage_get(kind: offchain::StorageKind, key: &[u8]) -> Option<Vec<u8>> {
let mut len = 0u32;
unsafe {
let ptr = ext_local_storage_get.get()(
kind as u8 as u32,
key.as_ptr(),
key.len() as u32,
&mut len,
+9 -3
View File
@@ -288,15 +288,21 @@ impl offchain::Externalities for NeverOffchainExt {
unreachable!()
}
fn local_storage_set(&mut self, _key: &[u8], _value: &[u8]) {
fn local_storage_set(&mut self, _kind: offchain::StorageKind, _key: &[u8], _value: &[u8]) {
unreachable!()
}
fn local_storage_compare_and_set(&mut self, _key: &[u8], _old_value: &[u8], _new_value: &[u8]) {
fn local_storage_compare_and_set(
&mut self,
_kind: offchain::StorageKind,
_key: &[u8],
_old_value: &[u8],
_new_value: &[u8],
) -> bool {
unreachable!()
}
fn local_storage_get(&mut self, _key: &[u8]) -> Option<Vec<u8>> {
fn local_storage_get(&mut self, _kind: offchain::StorageKind, _key: &[u8]) -> Option<Vec<u8>> {
unreachable!()
}
+1 -1
View File
@@ -67,7 +67,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
// 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: 102,
impl_version: 102,
impl_version: 103,
apis: RUNTIME_API_VERSIONS,
};