Statement store (#13701)

* WIP Statement store

* Sync with networking changes in master

* WIP statement pallet

* Statement validation

* pallet tests

* Validation queue

* Store maintenance

* Basic statement refactoring + tests + docs

* Store metrics

* Store tests

* Store maintenance test

* cargo fmt

* Build fix

* OCW Api

* Offchain worker

* Enable host functions

* fmt

* Minor tweaks

* Fixed a warning

* Removed tracing

* Manual expiration

* Reworked constraint management

* Updated pallet constraint calculation

* Added small test

* Added remove function to the APIs

* Copy-paste spec into readme

* Comments

* Made the store optional

* Removed network protocol controller

* fmt

* Clippy fixes

* fmt

* fmt

* More clippy fixes

* More clippy fixes

* More clippy fixes

* Update client/statement-store/README.md

Co-authored-by: cheme <emericchevalier.pro@gmail.com>

* Apply suggestions from code review

Co-authored-by: Bastian Köcher <git@kchr.de>

* Removed sstore from node-template

* Sort out data path

* Added offline check

* Removed dispatch_statement

* Renamed into_generic

* Fixed commit placement

* Use HashSet for tracking peers/statements

* fmt

* Use ExtendedHostFunctions

* Fixed benches

* Tweaks

* Apply suggestions from code review

Co-authored-by: cheme <emericchevalier.pro@gmail.com>

* Fixed priority mixup

* Rename

* newtypes for priorities

* Added MAX_TOPICS

* Fixed key filtering logic

* Remove empty entrie

* Removed prefix from signing

* More documentation

* fmt

* Moved store setup from sc-service to node

* Handle maintenance task in sc-statement-store

* Use statement iterator

* Renamed runtime API mod

* fmt

* Remove dump_encoded

* fmt

* Apply suggestions from code review

Co-authored-by: Bastian Köcher <git@kchr.de>

* Apply suggestions from code review

Co-authored-by: Bastian Köcher <git@kchr.de>

* Fixed build after applying review suggestions

* License exceptions

* fmt

* Store options

* Moved pallet consts to config trait

* Removed global priority

* Validate fields when decoding

* Limit validation channel size

* Made a comment into module doc

* Removed submit_encoded

---------

Co-authored-by: cheme <emericchevalier.pro@gmail.com>
Co-authored-by: Bastian Köcher <git@kchr.de>
This commit is contained in:
Arkadiy Paronyan
2023-05-04 12:24:32 +02:00
committed by GitHub
parent 5a1074712a
commit bfafbf7bac
48 changed files with 3911 additions and 26 deletions
+88
View File
@@ -3783,6 +3783,7 @@ dependencies = [
"pallet-staking-reward-curve",
"pallet-staking-runtime-api",
"pallet-state-trie-migration",
"pallet-statement",
"pallet-sudo",
"pallet-timestamp",
"pallet-tips",
@@ -3808,6 +3809,7 @@ dependencies = [
"sp-runtime",
"sp-session",
"sp-staking",
"sp-statement-store",
"sp-std",
"sp-transaction-pool",
"sp-version",
@@ -5120,10 +5122,12 @@ dependencies = [
"sc-keystore",
"sc-network",
"sc-network-common",
"sc-network-statement",
"sc-network-sync",
"sc-rpc",
"sc-service",
"sc-service-test",
"sc-statement-store",
"sc-storage-monitor",
"sc-sync-state-rpc",
"sc-sysinfo",
@@ -5192,6 +5196,7 @@ dependencies = [
"sp-keystore",
"sp-runtime",
"sp-state-machine",
"sp-statement-store",
"sp-tracing",
"sp-trie",
"wat",
@@ -5251,6 +5256,7 @@ dependencies = [
"sp-consensus-babe",
"sp-keystore",
"sp-runtime",
"sp-statement-store",
"substrate-frame-rpc-system",
"substrate-state-trie-migration-rpc",
]
@@ -5288,6 +5294,7 @@ dependencies = [
"sc-rpc",
"sc-rpc-api",
"sc-service",
"sc-statement-store",
"sc-telemetry",
"sc-transaction-pool",
"sc-transaction-pool-api",
@@ -6990,6 +6997,24 @@ dependencies = [
"zstd 0.12.3+zstd.1.5.2",
]
[[package]]
name = "pallet-statement"
version = "4.0.0-dev"
dependencies = [
"frame-support",
"frame-system",
"log",
"pallet-balances",
"parity-scale-codec",
"scale-info",
"sp-api",
"sp-core",
"sp-io",
"sp-runtime",
"sp-statement-store",
"sp-std",
]
[[package]]
name = "pallet-sudo"
version = "4.0.0-dev"
@@ -8618,6 +8643,7 @@ dependencies = [
"sp-keystore",
"sp-runtime",
"sp-state-machine",
"sp-statement-store",
"sp-storage",
"sp-test-primitives",
"substrate-prometheus-endpoint",
@@ -9293,6 +9319,26 @@ dependencies = [
"thiserror",
]
[[package]]
name = "sc-network-statement"
version = "0.10.0-dev"
dependencies = [
"array-bytes",
"async-channel",
"futures",
"libp2p",
"log",
"parity-scale-codec",
"pin-project",
"sc-network",
"sc-network-common",
"sc-peerset",
"sp-consensus",
"sp-runtime",
"sp-statement-store",
"substrate-prometheus-endpoint",
]
[[package]]
name = "sc-network-sync"
version = "0.10.0-dev"
@@ -9474,6 +9520,7 @@ dependencies = [
"sp-rpc",
"sp-runtime",
"sp-session",
"sp-statement-store",
"sp-version",
"substrate-test-runtime-client",
"tokio",
@@ -9672,6 +9719,30 @@ dependencies = [
"sp-core",
]
[[package]]
name = "sc-statement-store"
version = "4.0.0-dev"
dependencies = [
"async-trait",
"env_logger 0.9.3",
"futures",
"futures-timer",
"log",
"parity-db",
"parity-scale-codec",
"parking_lot 0.12.1",
"sc-client-api",
"sp-api",
"sp-blockchain",
"sp-core",
"sp-runtime",
"sp-statement-store",
"sp-tracing",
"substrate-prometheus-endpoint",
"tempfile",
"tokio",
]
[[package]]
name = "sc-storage-monitor"
version = "0.1.0"
@@ -10980,6 +11051,23 @@ dependencies = [
"trie-db",
]
[[package]]
name = "sp-statement-store"
version = "4.0.0-dev"
dependencies = [
"log",
"parity-scale-codec",
"scale-info",
"sp-api",
"sp-application-crypto",
"sp-core",
"sp-externalities",
"sp-runtime",
"sp-runtime-interface",
"sp-std",
"thiserror",
]
[[package]]
name = "sp-std"
version = "5.0.0"
+4
View File
@@ -46,6 +46,7 @@ members = [
"client/merkle-mountain-range/rpc",
"client/network",
"client/network/transactions",
"client/network/statement",
"client/network-gossip",
"client/network/bitswap",
"client/network/common",
@@ -63,6 +64,7 @@ members = [
"client/service",
"client/service/test",
"client/state-db",
"client/statement-store",
"client/storage-monitor",
"client/sysinfo",
"client/sync-state-rpc",
@@ -151,6 +153,7 @@ members = [
"frame/sudo",
"frame/root-offences",
"frame/root-testing",
"frame/statement",
"frame/support",
"frame/support/procedural",
"frame/support/procedural/tools",
@@ -220,6 +223,7 @@ members = [
"primitives/session",
"primitives/staking",
"primitives/state-machine",
"primitives/statement-store",
"primitives/std",
"primitives/storage",
"primitives/test-primitives",
@@ -28,6 +28,7 @@ sc-telemetry = { version = "4.0.0-dev", path = "../../../client/telemetry" }
sc-keystore = { version = "4.0.0-dev", path = "../../../client/keystore" }
sc-transaction-pool = { version = "4.0.0-dev", path = "../../../client/transaction-pool" }
sc-transaction-pool-api = { version = "4.0.0-dev", path = "../../../client/transaction-pool/api" }
sc-statement-store = { version = "4.0.0-dev", path = "../../../client/statement-store" }
sc-consensus-aura = { version = "0.10.0-dev", path = "../../../client/consensus/aura" }
sp-consensus-aura = { version = "0.10.0-dev", path = "../../../primitives/consensus/aura" }
sp-consensus = { version = "0.10.0-dev", path = "../../../primitives/consensus/common" }
+2
View File
@@ -66,9 +66,11 @@ sc-chain-spec = { version = "4.0.0-dev", path = "../../../client/chain-spec" }
sc-consensus = { version = "0.10.0-dev", path = "../../../client/consensus/common" }
sc-transaction-pool = { version = "4.0.0-dev", path = "../../../client/transaction-pool" }
sc-transaction-pool-api = { version = "4.0.0-dev", path = "../../../client/transaction-pool/api" }
sc-statement-store = { version = "4.0.0-dev", path = "../../../client/statement-store" }
sc-network = { version = "0.10.0-dev", path = "../../../client/network" }
sc-network-common = { version = "0.10.0-dev", path = "../../../client/network/common" }
sc-network-sync = { version = "0.10.0-dev", path = "../../../client/network/sync" }
sc-network-statement = { version = "0.10.0-dev", path = "../../../client/network/statement" }
sc-consensus-slots = { version = "0.10.0-dev", path = "../../../client/consensus/slots" }
sc-consensus-babe = { version = "0.10.0-dev", path = "../../../client/consensus/babe" }
grandpa = { version = "0.10.0-dev", package = "sc-consensus-grandpa", path = "../../../client/consensus/grandpa" }
@@ -104,7 +104,8 @@ fn new_node(tokio_handle: Handle) -> node_cli::service::NewFullBase {
max_runtime_instances: 8,
runtime_cache_size: 2,
announce_block: true,
base_path: Some(base_path),
data_path: base_path.path().into(),
base_path,
informant_output_format: Default::default(),
wasm_runtime_overrides: None,
};
@@ -98,7 +98,8 @@ fn new_node(tokio_handle: Handle) -> node_cli::service::NewFullBase {
max_runtime_instances: 8,
runtime_cache_size: 2,
announce_block: true,
base_path: Some(base_path),
data_path: base_path.path().into(),
base_path,
informant_output_format: Default::default(),
wasm_runtime_overrides: None,
};
+46 -3
View File
@@ -35,6 +35,7 @@ use sc_network::{event::Event, NetworkEventStream, NetworkService};
use sc_network_common::sync::warp::WarpSyncParams;
use sc_network_sync::SyncingService;
use sc_service::{config::Configuration, error::Error as ServiceError, RpcHandlers, TaskManager};
use sc_statement_store::Store as StatementStore;
use sc_telemetry::{Telemetry, TelemetryWorker};
use sp_api::ProvideRuntimeApi;
use sp_core::crypto::Pair;
@@ -148,6 +149,7 @@ pub fn new_partial(
),
grandpa::SharedVoterState,
Option<Telemetry>,
Arc<StatementStore>,
),
>,
ServiceError,
@@ -227,6 +229,15 @@ pub fn new_partial(
let import_setup = (block_import, grandpa_link, babe_link);
let statement_store = sc_statement_store::Store::new_shared(
&config.data_path,
Default::default(),
client.clone(),
config.prometheus_registry(),
&task_manager.spawn_handle(),
)
.map_err(|e| ServiceError::Other(format!("Statement store error: {:?}", e)))?;
let (rpc_extensions_builder, rpc_setup) = {
let (_, grandpa_link, _) = &import_setup;
@@ -247,6 +258,7 @@ pub fn new_partial(
let chain_spec = config.chain_spec.cloned_box();
let rpc_backend = backend.clone();
let rpc_statement_store = statement_store.clone();
let rpc_extensions_builder = move |deny_unsafe, subscription_executor| {
let deps = node_rpc::FullDeps {
client: client.clone(),
@@ -265,6 +277,7 @@ pub fn new_partial(
subscription_executor,
finality_provider: finality_proof_provider.clone(),
},
statement_store: rpc_statement_store.clone(),
};
node_rpc::create_full(deps, rpc_backend.clone()).map_err(Into::into)
@@ -281,7 +294,7 @@ pub fn new_partial(
select_chain,
import_queue,
transaction_pool,
other: (rpc_extensions_builder, import_setup, rpc_setup, telemetry),
other: (rpc_extensions_builder, import_setup, rpc_setup, telemetry, statement_store),
})
}
@@ -325,7 +338,7 @@ pub fn new_full_base(
keystore_container,
select_chain,
transaction_pool,
other: (rpc_builder, import_setup, rpc_setup, mut telemetry),
other: (rpc_builder, import_setup, rpc_setup, mut telemetry, statement_store),
} = new_partial(&config)?;
let shared_voter_state = rpc_setup;
@@ -335,6 +348,16 @@ pub fn new_full_base(
&config.chain_spec,
);
let statement_handler_proto = sc_network_statement::StatementHandlerPrototype::new(
client
.block_hash(0u32.into())
.ok()
.flatten()
.expect("Genesis block exists; qed"),
config.chain_spec.fork_id(),
);
config.network.extra_sets.push(statement_handler_proto.set_config());
config
.network
.extra_sets
@@ -526,7 +549,7 @@ pub fn new_full_base(
sync: Arc::new(sync_service.clone()),
telemetry: telemetry.as_ref().map(|x| x.handle()),
voting_rule: grandpa::VotingRulesBuilder::default().build(),
prometheus_registry,
prometheus_registry: prometheus_registry.clone(),
shared_voter_state,
};
@@ -539,6 +562,26 @@ pub fn new_full_base(
);
}
// Spawn statement protocol worker
let statement_protocol_executor = {
let spawn_handle = task_manager.spawn_handle();
Box::new(move |fut| {
spawn_handle.spawn("network-statement-validator", Some("networking"), fut);
})
};
let statement_handler = statement_handler_proto.build(
network.clone(),
sync_service.clone(),
statement_store.clone(),
prometheus_registry.as_ref(),
statement_protocol_executor,
)?;
task_manager.spawn_handle().spawn(
"network-statement-handler",
Some("networking"),
statement_handler.run(),
);
network_starter.start_network();
Ok(NewFullBase {
task_manager,
+1
View File
@@ -24,6 +24,7 @@ sp-keystore = { version = "0.13.0", path = "../../../primitives/keystore" }
sp-state-machine = { version = "0.13.0", path = "../../../primitives/state-machine" }
sp-tracing = { version = "6.0.0", path = "../../../primitives/tracing" }
sp-trie = { version = "7.0.0", path = "../../../primitives/trie" }
sp-statement-store = { version = "4.0.0-dev", path = "../../../primitives/statement-store" }
[dev-dependencies]
criterion = "0.4.0"
+4 -1
View File
@@ -25,7 +25,10 @@ pub use sc_executor::NativeElseWasmExecutor;
pub struct ExecutorDispatch;
impl sc_executor::NativeExecutionDispatch for ExecutorDispatch {
type ExtendHostFunctions = frame_benchmarking::benchmarking::HostFunctions;
type ExtendHostFunctions = (
frame_benchmarking::benchmarking::HostFunctions,
sp_statement_store::runtime_api::HostFunctions,
);
fn dispatch(method: &str, data: &[u8]) -> Option<Vec<u8>> {
kitchensink_runtime::api::dispatch(method, data)
+1
View File
@@ -35,5 +35,6 @@ sp-consensus = { version = "0.10.0-dev", path = "../../../primitives/consensus/c
sp-consensus-babe = { version = "0.10.0-dev", path = "../../../primitives/consensus/babe" }
sp-keystore = { version = "0.13.0", path = "../../../primitives/keystore" }
sp-runtime = { version = "7.0.0", path = "../../../primitives/runtime" }
sp-statement-store = { version = "4.0.0-dev", path = "../../../primitives/statement-store" }
substrate-frame-rpc-system = { version = "4.0.0-dev", path = "../../../utils/frame/rpc/system" }
substrate-state-trie-migration-rpc = { version = "4.0.0-dev", path = "../../../utils/frame/rpc/state-trie-migration-rpc/" }
+19 -2
View File
@@ -88,6 +88,8 @@ pub struct FullDeps<C, P, SC, B> {
pub babe: BabeDeps,
/// GRANDPA specific dependencies.
pub grandpa: GrandpaDeps<B>,
/// Shared statement store reference.
pub statement_store: Arc<dyn sp_statement_store::StatementStore>,
}
/// Instantiate all Full RPC extensions.
@@ -118,14 +120,26 @@ where
use pallet_transaction_payment_rpc::{TransactionPayment, TransactionPaymentApiServer};
use sc_consensus_babe_rpc::{Babe, BabeApiServer};
use sc_consensus_grandpa_rpc::{Grandpa, GrandpaApiServer};
use sc_rpc::dev::{Dev, DevApiServer};
use sc_rpc::{
dev::{Dev, DevApiServer},
statement::StatementApiServer,
};
use sc_rpc_spec_v2::chain_spec::{ChainSpec, ChainSpecApiServer};
use sc_sync_state_rpc::{SyncState, SyncStateApiServer};
use substrate_frame_rpc_system::{System, SystemApiServer};
use substrate_state_trie_migration_rpc::{StateMigration, StateMigrationApiServer};
let mut io = RpcModule::new(());
let FullDeps { client, pool, select_chain, chain_spec, deny_unsafe, babe, grandpa } = deps;
let FullDeps {
client,
pool,
select_chain,
chain_spec,
deny_unsafe,
babe,
grandpa,
statement_store,
} = deps;
let BabeDeps { keystore, babe_worker_handle } = babe;
let GrandpaDeps {
@@ -169,6 +183,9 @@ where
io.merge(StateMigration::new(client.clone(), backend, deny_unsafe).into_rpc())?;
io.merge(Dev::new(client, deny_unsafe).into_rpc())?;
let statement_store =
sc_rpc::statement::StatementStore::new(statement_store, deny_unsafe).into_rpc();
io.merge(statement_store)?;
Ok(io)
}
+5
View File
@@ -39,6 +39,7 @@ sp-runtime = { version = "7.0.0", default-features = false, path = "../../../pri
sp-staking = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/staking" }
sp-session = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/session" }
sp-transaction-pool = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/transaction-pool" }
sp-statement-store = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/statement-store" }
sp-version = { version = "5.0.0", default-features = false, path = "../../../primitives/version" }
sp-io = { version = "7.0.0", default-features = false, path = "../../../primitives/io" }
@@ -105,6 +106,7 @@ pallet-staking = { version = "4.0.0-dev", default-features = false, path = "../.
pallet-staking-reward-curve = { version = "4.0.0-dev", default-features = false, path = "../../../frame/staking/reward-curve" }
pallet-staking-runtime-api = { version = "4.0.0-dev", default-features = false, path = "../../../frame/staking/runtime-api" }
pallet-state-trie-migration = { version = "4.0.0-dev", default-features = false, path = "../../../frame/state-trie-migration" }
pallet-statement = { version = "4.0.0-dev", default-features = false, path = "../../../frame/statement" }
pallet-scheduler = { version = "4.0.0-dev", default-features = false, path = "../../../frame/scheduler" }
pallet-society = { version = "4.0.0-dev", default-features = false, path = "../../../frame/society" }
pallet-sudo = { version = "4.0.0-dev", default-features = false, path = "../../../frame/sudo" }
@@ -187,6 +189,7 @@ std = [
"pallet-staking/std",
"pallet-staking-runtime-api/std",
"pallet-state-trie-migration/std",
"pallet-statement/std",
"pallet-salary/std",
"sp-session/std",
"pallet-sudo/std",
@@ -204,6 +207,7 @@ std = [
"pallet-treasury/std",
"pallet-asset-rate/std",
"sp-transaction-pool/std",
"sp-statement-store/std",
"pallet-utility/std",
"sp-version/std",
"pallet-society/std",
@@ -330,6 +334,7 @@ try-runtime = [
"pallet-session/try-runtime",
"pallet-staking/try-runtime",
"pallet-state-trie-migration/try-runtime",
"pallet-statement/try-runtime",
"pallet-scheduler/try-runtime",
"pallet-society/try-runtime",
"pallet-sudo/try-runtime",
+30
View File
@@ -1755,6 +1755,26 @@ impl frame_benchmarking_pallet_pov::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
}
parameter_types! {
pub StatementCost: Balance = 1 * DOLLARS;
pub StatementByteCost: Balance = 100 * MILLICENTS;
pub const MinAllowedStatements: u32 = 4;
pub const MaxAllowedStatements: u32 = 10;
pub const MinAllowedBytes: u32 = 1024;
pub const MaxAllowedBytes: u32 = 4096;
}
impl pallet_statement::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type Currency = Balances;
type StatementCost = StatementCost;
type ByteCost = StatementByteCost;
type MinAllowedStatements = MinAllowedStatements;
type MaxAllowedStatements = MaxAllowedStatements;
type MinAllowedBytes = MinAllowedBytes;
type MaxAllowedBytes = MaxAllowedBytes;
}
construct_runtime!(
pub struct Runtime where
Block = Block,
@@ -1826,6 +1846,7 @@ construct_runtime!(
FastUnstake: pallet_fast_unstake,
MessageQueue: pallet_message_queue,
Pov: frame_benchmarking_pallet_pov,
Statement: pallet_statement,
}
);
@@ -2011,6 +2032,15 @@ impl_runtime_apis! {
}
}
impl sp_statement_store::runtime_api::ValidateStatement<Block> for Runtime {
fn validate_statement(
source: sp_statement_store::runtime_api::StatementSource,
statement: sp_statement_store::Statement,
) -> Result<sp_statement_store::runtime_api::ValidStatement, sp_statement_store::runtime_api::InvalidStatement> {
Statement::validate_statement(source, statement)
}
}
impl sp_offchain::OffchainWorkerApi<Block> for Runtime {
fn offchain_worker(header: &<Block as BlockT>::Header) {
Executive::offchain_worker(header)
+1
View File
@@ -34,6 +34,7 @@ sp-externalities = { version = "0.13.0", path = "../../primitives/externalities"
sp-keystore = { version = "0.13.0", default-features = false, path = "../../primitives/keystore" }
sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime" }
sp-state-machine = { version = "0.13.0", path = "../../primitives/state-machine" }
sp-statement-store = { version = "4.0.0-dev", path = "../../primitives/statement-store" }
sp-storage = { version = "7.0.0", path = "../../primitives/storage" }
[dev-dependencies]
@@ -166,7 +166,7 @@ pub struct ExecutionExtensions<Block: BlockT> {
strategies: ExecutionStrategies,
keystore: Option<KeystorePtr>,
offchain_db: Option<Box<dyn DbExternalitiesFactory>>,
// FIXME: these two are only RwLock because of https://github.com/paritytech/substrate/issues/4587
// FIXME: these three are only RwLock because of https://github.com/paritytech/substrate/issues/4587
// remove when fixed.
// To break retain cycle between `Client` and `TransactionPool` we require this
// extension to be a `Weak` reference.
@@ -174,6 +174,7 @@ pub struct ExecutionExtensions<Block: BlockT> {
// during initialization.
transaction_pool: RwLock<Option<Weak<dyn OffchainSubmitTransaction<Block>>>>,
extensions_factory: RwLock<Box<dyn ExtensionsFactory<Block>>>,
statement_store: RwLock<Option<Weak<dyn sp_statement_store::StatementStore>>>,
read_runtime_version: Arc<dyn ReadRuntimeVersion>,
}
@@ -186,6 +187,7 @@ impl<Block: BlockT> ExecutionExtensions<Block> {
read_runtime_version: Arc<dyn ReadRuntimeVersion>,
) -> Self {
let transaction_pool = RwLock::new(None);
let statement_store = RwLock::new(None);
let extensions_factory = Box::new(());
Self {
strategies,
@@ -193,6 +195,7 @@ impl<Block: BlockT> ExecutionExtensions<Block> {
offchain_db,
extensions_factory: RwLock::new(extensions_factory),
transaction_pool,
statement_store,
read_runtime_version,
}
}
@@ -215,6 +218,11 @@ impl<Block: BlockT> ExecutionExtensions<Block> {
*self.transaction_pool.write() = Some(Arc::downgrade(pool) as _);
}
/// Register statement store extension.
pub fn register_statement_store(&self, store: Arc<dyn sp_statement_store::StatementStore>) {
*self.statement_store.write() = Some(Arc::downgrade(&store) as _);
}
/// Based on the execution context and capabilities it produces
/// the extensions object to support desired set of APIs.
pub fn extensions(
@@ -245,6 +253,11 @@ impl<Block: BlockT> ExecutionExtensions<Block> {
}
}
if capabilities.contains(offchain::Capabilities::STATEMENT_STORE) {
if let Some(store) = self.statement_store.read().as_ref().and_then(|x| x.upgrade()) {
extensions.register(sp_statement_store::runtime_api::StatementStoreExt(store));
}
}
if capabilities.contains(offchain::Capabilities::OFFCHAIN_DB_READ) ||
capabilities.contains(offchain::Capabilities::OFFCHAIN_DB_WRITE)
{
+2 -1
View File
@@ -491,6 +491,7 @@ pub trait CliConfiguration<DCV: DefaultConfigurationValues = ()>: Sized {
)?,
keystore,
database: self.database_config(&config_dir, database_cache_size, database)?,
data_path: config_dir,
trie_cache_maximum_size: self.trie_cache_maximum_size()?,
state_pruning: self.state_pruning()?,
blocks_pruning: self.blocks_pruning()?,
@@ -519,7 +520,7 @@ pub trait CliConfiguration<DCV: DefaultConfigurationValues = ()>: Sized {
max_runtime_instances,
announce_block: self.announce_block()?,
role,
base_path: Some(base_path),
base_path,
informant_output_format: Default::default(),
runtime_cache_size,
})
+4 -2
View File
@@ -259,6 +259,7 @@ mod tests {
fn create_runner() -> Runner<Cli> {
let runtime = build_runtime().unwrap();
let root = PathBuf::from("db");
let runner = Runner::new(
Configuration {
impl_name: "spec".into(),
@@ -268,7 +269,7 @@ mod tests {
transaction_pool: Default::default(),
network: NetworkConfiguration::new_memory(),
keystore: sc_service::config::KeystoreConfig::InMemory,
database: sc_client_db::DatabaseSource::ParityDb { path: PathBuf::from("db") },
database: sc_client_db::DatabaseSource::ParityDb { path: root.clone() },
trie_cache_maximum_size: None,
state_pruning: None,
blocks_pruning: sc_client_db::BlocksPruning::KeepAll,
@@ -306,7 +307,8 @@ mod tests {
tracing_receiver: Default::default(),
max_runtime_instances: 8,
announce_block: true,
base_path: None,
base_path: sc_service::BasePath::new(root.clone()),
data_path: root,
informant_output_format: Default::default(),
runtime_cache_size: 2,
},
@@ -0,0 +1,29 @@
[package]
description = "Substrate statement protocol"
name = "sc-network-statement"
version = "0.10.0-dev"
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2021"
homepage = "https://substrate.io"
repository = "https://github.com/paritytech/substrate/"
documentation = "https://docs.rs/sc-network-statement"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
array-bytes = "4.1"
async-channel = "1.8.0"
codec = { package = "parity-scale-codec", version = "3.2.2", features = ["derive"] }
futures = "0.3.21"
libp2p = "0.50.0"
log = "0.4.17"
pin-project = "1.0.12"
prometheus-endpoint = { package = "substrate-prometheus-endpoint", version = "0.10.0-dev", path = "../../../utils/prometheus" }
sc-network-common = { version = "0.10.0-dev", path = "../common" }
sc-network = { version = "0.10.0-dev", path = "../" }
sc-peerset = { version = "4.0.0-dev", path = "../../peerset" }
sp-runtime = { version = "7.0.0", path = "../../../primitives/runtime" }
sp-consensus = { version = "0.10.0-dev", path = "../../../primitives/consensus/common" }
sp-statement-store = { version = "4.0.0-dev", path = "../../../primitives/statement-store" }
@@ -0,0 +1,33 @@
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program 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.
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
//! Configuration of the statement protocol
use std::time;
/// Interval at which we propagate statements;
pub(crate) const PROPAGATE_TIMEOUT: time::Duration = time::Duration::from_millis(1000);
/// Maximum number of known statement hashes to keep for a peer.
pub(crate) const MAX_KNOWN_STATEMENTS: usize = 10240;
/// Maximum allowed size for a statement notification.
pub(crate) const MAX_STATEMENT_SIZE: u64 = 256 * 1024;
/// Maximum number of statement validation request we keep at any moment.
pub(crate) const MAX_PENDING_STATEMENTS: usize = 8192;
@@ -0,0 +1,486 @@
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program 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.
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
//! Statement handling to plug on top of the network service.
//!
//! Usage:
//!
//! - Use [`StatementHandlerPrototype::new`] to create a prototype.
//! - Pass the return value of [`StatementHandlerPrototype::set_config`] to the network
//! configuration as an extra peers set.
//! - Use [`StatementHandlerPrototype::build`] then [`StatementHandler::run`] to obtain a
//! `Future` that processes statements.
use crate::config::*;
use codec::{Decode, Encode};
use futures::{channel::oneshot, prelude::*, stream::FuturesUnordered, FutureExt};
use libp2p::{multiaddr, PeerId};
use prometheus_endpoint::{register, Counter, PrometheusError, Registry, U64};
use sc_network::{
config::{NonDefaultSetConfig, NonReservedPeerMode, SetConfig},
error,
event::Event,
types::ProtocolName,
utils::{interval, LruHashSet},
NetworkEventStream, NetworkNotification, NetworkPeers,
};
use sc_network_common::{
role::ObservedRole,
sync::{SyncEvent, SyncEventStream},
};
use sp_statement_store::{
Hash, NetworkPriority, Statement, StatementSource, StatementStore, SubmitResult,
};
use std::{
collections::{hash_map::Entry, HashMap, HashSet},
iter,
num::NonZeroUsize,
pin::Pin,
sync::Arc,
};
pub mod config;
/// A set of statements.
pub type Statements = Vec<Statement>;
/// Future resolving to statement import result.
pub type StatementImportFuture = oneshot::Receiver<SubmitResult>;
mod rep {
use sc_peerset::ReputationChange as Rep;
/// Reputation change when a peer sends us any statement.
///
/// This forces node to verify it, thus the negative value here. Once statement is verified,
/// reputation change should be refunded with `ANY_STATEMENT_REFUND`
pub const ANY_STATEMENT: Rep = Rep::new(-(1 << 4), "Any statement");
/// Reputation change when a peer sends us any statement that is not invalid.
pub const ANY_STATEMENT_REFUND: Rep = Rep::new(1 << 4, "Any statement (refund)");
/// Reputation change when a peer sends us an statement that we didn't know about.
pub const GOOD_STATEMENT: Rep = Rep::new(1 << 7, "Good statement");
/// Reputation change when a peer sends us a bad statement.
pub const BAD_STATEMENT: Rep = Rep::new(-(1 << 12), "Bad statement");
/// Reputation change when a peer sends us a duplicate statement.
pub const DUPLICATE_STATEMENT: Rep = Rep::new(-(1 << 7), "Duplicate statement");
/// Reputation change when a peer sends us particularly useful statement
pub const EXCELLENT_STATEMENT: Rep = Rep::new(1 << 8, "High priority statement");
}
const LOG_TARGET: &str = "statement-gossip";
struct Metrics {
propagated_statements: Counter<U64>,
}
impl Metrics {
fn register(r: &Registry) -> Result<Self, PrometheusError> {
Ok(Self {
propagated_statements: register(
Counter::new(
"substrate_sync_propagated_statements",
"Number of statements propagated to at least one peer",
)?,
r,
)?,
})
}
}
/// Prototype for a [`StatementHandler`].
pub struct StatementHandlerPrototype {
protocol_name: ProtocolName,
}
impl StatementHandlerPrototype {
/// Create a new instance.
pub fn new<Hash: AsRef<[u8]>>(genesis_hash: Hash, fork_id: Option<&str>) -> Self {
let genesis_hash = genesis_hash.as_ref();
let protocol_name = if let Some(fork_id) = fork_id {
format!("/{}/{}/statement/1", array_bytes::bytes2hex("", genesis_hash), fork_id)
} else {
format!("/{}/statement/1", array_bytes::bytes2hex("", genesis_hash))
};
Self { protocol_name: protocol_name.into() }
}
/// Returns the configuration of the set to put in the network configuration.
pub fn set_config(&self) -> NonDefaultSetConfig {
NonDefaultSetConfig {
notifications_protocol: self.protocol_name.clone(),
fallback_names: Vec::new(),
max_notification_size: MAX_STATEMENT_SIZE,
handshake: None,
set_config: SetConfig {
in_peers: 0,
out_peers: 0,
reserved_nodes: Vec::new(),
non_reserved_mode: NonReservedPeerMode::Deny,
},
}
}
/// Turns the prototype into the actual handler.
///
/// Important: the statements handler is initially disabled and doesn't gossip statements.
/// Gossiping is enabled when major syncing is done.
pub fn build<
N: NetworkPeers + NetworkEventStream + NetworkNotification,
S: SyncEventStream + sp_consensus::SyncOracle,
>(
self,
network: N,
sync: S,
statement_store: Arc<dyn StatementStore>,
metrics_registry: Option<&Registry>,
executor: impl Fn(Pin<Box<dyn Future<Output = ()> + Send>>) + Send,
) -> error::Result<StatementHandler<N, S>> {
let net_event_stream = network.event_stream("statement-handler-net");
let sync_event_stream = sync.event_stream("statement-handler-sync");
let (queue_sender, mut queue_receiver) = async_channel::bounded(100_000);
let store = statement_store.clone();
executor(
async move {
loop {
let task: Option<(Statement, oneshot::Sender<SubmitResult>)> =
queue_receiver.next().await;
match task {
None => return,
Some((statement, completion)) => {
let result = store.submit(statement, StatementSource::Network);
if completion.send(result).is_err() {
log::debug!(
target: LOG_TARGET,
"Error sending validation completion"
);
}
},
}
}
}
.boxed(),
);
let handler = StatementHandler {
protocol_name: self.protocol_name,
propagate_timeout: (Box::pin(interval(PROPAGATE_TIMEOUT))
as Pin<Box<dyn Stream<Item = ()> + Send>>)
.fuse(),
pending_statements: FuturesUnordered::new(),
pending_statements_peers: HashMap::new(),
network,
sync,
net_event_stream: net_event_stream.fuse(),
sync_event_stream: sync_event_stream.fuse(),
peers: HashMap::new(),
statement_store,
queue_sender,
metrics: if let Some(r) = metrics_registry {
Some(Metrics::register(r)?)
} else {
None
},
};
Ok(handler)
}
}
/// Handler for statements. Call [`StatementHandler::run`] to start the processing.
pub struct StatementHandler<
N: NetworkPeers + NetworkEventStream + NetworkNotification,
S: SyncEventStream + sp_consensus::SyncOracle,
> {
protocol_name: ProtocolName,
/// Interval at which we call `propagate_statements`.
propagate_timeout: stream::Fuse<Pin<Box<dyn Stream<Item = ()> + Send>>>,
/// Pending statements verification tasks.
pending_statements:
FuturesUnordered<Pin<Box<dyn Future<Output = (Hash, Option<SubmitResult>)> + Send>>>,
/// As multiple peers can send us the same statement, we group
/// these peers using the statement hash while the statement is
/// imported. This prevents that we import the same statement
/// multiple times concurrently.
pending_statements_peers: HashMap<Hash, HashSet<PeerId>>,
/// Network service to use to send messages and manage peers.
network: N,
/// Syncing service.
sync: S,
/// Stream of networking events.
net_event_stream: stream::Fuse<Pin<Box<dyn Stream<Item = Event> + Send>>>,
/// Receiver for syncing-related events.
sync_event_stream: stream::Fuse<Pin<Box<dyn Stream<Item = SyncEvent> + Send>>>,
// All connected peers
peers: HashMap<PeerId, Peer>,
statement_store: Arc<dyn StatementStore>,
queue_sender: async_channel::Sender<(Statement, oneshot::Sender<SubmitResult>)>,
/// Prometheus metrics.
metrics: Option<Metrics>,
}
/// Peer information
#[derive(Debug)]
struct Peer {
/// Holds a set of statements known to this peer.
known_statements: LruHashSet<Hash>,
role: ObservedRole,
}
impl<N, S> StatementHandler<N, S>
where
N: NetworkPeers + NetworkEventStream + NetworkNotification,
S: SyncEventStream + sp_consensus::SyncOracle,
{
/// Turns the [`StatementHandler`] into a future that should run forever and not be
/// interrupted.
pub async fn run(mut self) {
loop {
futures::select! {
_ = self.propagate_timeout.next() => {
self.propagate_statements();
},
(hash, result) = self.pending_statements.select_next_some() => {
if let Some(peers) = self.pending_statements_peers.remove(&hash) {
if let Some(result) = result {
peers.into_iter().for_each(|p| self.on_handle_statement_import(p, &result));
}
} else {
log::warn!(target: LOG_TARGET, "Inconsistent state, no peers for pending statement!");
}
},
network_event = self.net_event_stream.next() => {
if let Some(network_event) = network_event {
self.handle_network_event(network_event).await;
} else {
// Networking has seemingly closed. Closing as well.
return;
}
},
sync_event = self.sync_event_stream.next() => {
if let Some(sync_event) = sync_event {
self.handle_sync_event(sync_event);
} else {
// Syncing has seemingly closed. Closing as well.
return;
}
}
}
}
}
fn handle_sync_event(&mut self, event: SyncEvent) {
match event {
SyncEvent::PeerConnected(remote) => {
let addr = iter::once(multiaddr::Protocol::P2p(remote.into()))
.collect::<multiaddr::Multiaddr>();
let result = self.network.add_peers_to_reserved_set(
self.protocol_name.clone(),
iter::once(addr).collect(),
);
if let Err(err) = result {
log::error!(target: LOG_TARGET, "Add reserved peer failed: {}", err);
}
},
SyncEvent::PeerDisconnected(remote) => {
self.network.remove_peers_from_reserved_set(
self.protocol_name.clone(),
iter::once(remote).collect(),
);
},
}
}
async fn handle_network_event(&mut self, event: Event) {
match event {
Event::Dht(_) => {},
Event::NotificationStreamOpened { remote, protocol, role, .. }
if protocol == self.protocol_name =>
{
let _was_in = self.peers.insert(
remote,
Peer {
known_statements: LruHashSet::new(
NonZeroUsize::new(MAX_KNOWN_STATEMENTS).expect("Constant is nonzero"),
),
role,
},
);
debug_assert!(_was_in.is_none());
},
Event::NotificationStreamClosed { remote, protocol }
if protocol == self.protocol_name =>
{
let _peer = self.peers.remove(&remote);
debug_assert!(_peer.is_some());
},
Event::NotificationsReceived { remote, messages } => {
for (protocol, message) in messages {
if protocol != self.protocol_name {
continue
}
// Accept statements only when node is not major syncing
if self.sync.is_major_syncing() {
log::trace!(
target: LOG_TARGET,
"{remote}: Ignoring statements while major syncing or offline"
);
continue
}
if let Ok(statements) = <Statements as Decode>::decode(&mut message.as_ref()) {
self.on_statements(remote, statements);
} else {
log::debug!(
target: LOG_TARGET,
"Failed to decode statement list from {remote}"
);
}
}
},
// Not our concern.
Event::NotificationStreamOpened { .. } | Event::NotificationStreamClosed { .. } => {},
}
}
/// Called when peer sends us new statements
fn on_statements(&mut self, who: PeerId, statements: Statements) {
log::trace!(target: LOG_TARGET, "Received {} statements from {}", statements.len(), who);
if let Some(ref mut peer) = self.peers.get_mut(&who) {
for s in statements {
if self.pending_statements.len() > MAX_PENDING_STATEMENTS {
log::debug!(
target: LOG_TARGET,
"Ignoring any further statements that exceed `MAX_PENDING_STATEMENTS`({}) limit",
MAX_PENDING_STATEMENTS,
);
break
}
let hash = s.hash();
peer.known_statements.insert(hash);
self.network.report_peer(who, rep::ANY_STATEMENT);
match self.pending_statements_peers.entry(hash) {
Entry::Vacant(entry) => {
let (completion_sender, completion_receiver) = oneshot::channel();
match self.queue_sender.try_send((s, completion_sender)) {
Ok(()) => {
self.pending_statements.push(
async move {
let res = completion_receiver.await;
(hash, res.ok())
}
.boxed(),
);
entry.insert(HashSet::from_iter([who]));
},
Err(async_channel::TrySendError::Full(_)) => {
log::debug!(
target: LOG_TARGET,
"Dropped statement because validation channel is full",
);
},
Err(async_channel::TrySendError::Closed(_)) => {
log::trace!(
target: LOG_TARGET,
"Dropped statement because validation channel is closed",
);
},
}
},
Entry::Occupied(mut entry) => {
if !entry.get_mut().insert(who) {
// Already received this from the same peer.
self.network.report_peer(who, rep::DUPLICATE_STATEMENT);
}
},
}
}
}
}
fn on_handle_statement_import(&mut self, who: PeerId, import: &SubmitResult) {
match import {
SubmitResult::New(NetworkPriority::High) =>
self.network.report_peer(who, rep::EXCELLENT_STATEMENT),
SubmitResult::New(NetworkPriority::Low) =>
self.network.report_peer(who, rep::GOOD_STATEMENT),
SubmitResult::Known => self.network.report_peer(who, rep::ANY_STATEMENT_REFUND),
SubmitResult::KnownExpired => {},
SubmitResult::Ignored => {},
SubmitResult::Bad(_) => self.network.report_peer(who, rep::BAD_STATEMENT),
SubmitResult::InternalError(_) => {},
}
}
/// Propagate one statement.
pub fn propagate_statement(&mut self, hash: &Hash) {
// Accept statements only when node is not major syncing
if self.sync.is_major_syncing() {
return
}
log::debug!(target: LOG_TARGET, "Propagating statement [{:?}]", hash);
if let Ok(Some(statement)) = self.statement_store.statement(hash) {
self.do_propagate_statements(&[(*hash, statement)]);
}
}
fn do_propagate_statements(&mut self, statements: &[(Hash, Statement)]) {
let mut propagated_statements = 0;
for (who, peer) in self.peers.iter_mut() {
// never send statements to light nodes
if matches!(peer.role, ObservedRole::Light) {
continue
}
let to_send = statements
.iter()
.filter_map(|(hash, stmt)| peer.known_statements.insert(*hash).then(|| stmt))
.collect::<Vec<_>>();
propagated_statements += to_send.len();
if !to_send.is_empty() {
log::trace!(target: LOG_TARGET, "Sending {} statements to {}", to_send.len(), who);
self.network
.write_notification(*who, self.protocol_name.clone(), to_send.encode());
}
}
if let Some(ref metrics) = self.metrics {
metrics.propagated_statements.inc_by(propagated_statements as _)
}
}
/// Call when we must propagate ready statements to peers.
fn propagate_statements(&mut self) {
// Send out statements only when node is not major syncing
if self.sync.is_major_syncing() {
return
}
log::debug!(target: LOG_TARGET, "Propagating statements");
if let Ok(statements) = self.statement_store.statements() {
self.do_propagate_statements(&statements);
}
}
}
+1
View File
@@ -32,4 +32,5 @@ pub mod child_state;
pub mod dev;
pub mod offchain;
pub mod state;
pub mod statement;
pub mod system;
@@ -0,0 +1,55 @@
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program 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.
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
//! Statement RPC errors.
use jsonrpsee::{
core::Error as JsonRpseeError,
types::error::{CallError, ErrorObject},
};
/// Statement RPC Result type.
pub type Result<T> = std::result::Result<T, Error>;
/// Statement RPC errors.
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// Statement store internal error.
#[error("Statement store error")]
StatementStore(String),
/// Call to an unsafe RPC was denied.
#[error(transparent)]
UnsafeRpcCalled(#[from] crate::policy::UnsafeRpcError),
}
/// Base error code for all statement errors.
const BASE_ERROR: i32 = 6000;
impl From<Error> for JsonRpseeError {
fn from(e: Error) -> Self {
match e {
Error::StatementStore(message) => CallError::Custom(ErrorObject::owned(
BASE_ERROR + 1,
format!("Statement store error: {message}"),
None::<()>,
))
.into(),
Error::UnsafeRpcCalled(e) => e.into(),
}
}
}
@@ -0,0 +1,60 @@
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program 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.
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
//! Substrate Statement Store RPC API.
use jsonrpsee::{core::RpcResult, proc_macros::rpc};
use sp_core::Bytes;
pub mod error;
/// Substrate statement RPC API
#[rpc(client, server)]
pub trait StatementApi {
/// Return all statements, SCALE-encoded.
#[method(name = "statement_dump")]
fn dump(&self) -> RpcResult<Vec<Bytes>>;
/// Return the data of all known statements which include all topics and have no `DecryptionKey`
/// field.
#[method(name = "statement_broadcasts")]
fn broadcasts(&self, match_all_topics: Vec<[u8; 32]>) -> RpcResult<Vec<Bytes>>;
/// Return the data of all known statements whose decryption key is identified as `dest` (this
/// will generally be the public key or a hash thereof for symmetric ciphers, or a hash of the
/// private key for symmetric ciphers).
#[method(name = "statement_posted")]
fn posted(&self, match_all_topics: Vec<[u8; 32]>, dest: [u8; 32]) -> RpcResult<Vec<Bytes>>;
/// Return the decrypted data of all known statements whose decryption key is identified as
/// `dest`. The key must be available to the client.
#[method(name = "statement_postedClear")]
fn posted_clear(
&self,
match_all_topics: Vec<[u8; 32]>,
dest: [u8; 32],
) -> RpcResult<Vec<Bytes>>;
/// Submit a pre-encoded statement.
#[method(name = "statement_submit")]
fn submit(&self, encoded: Bytes) -> RpcResult<()>;
/// Remove a statement from the store.
#[method(name = "statement_remove")]
fn remove(&self, statement_hash: [u8; 32]) -> RpcResult<()>;
}
+1
View File
@@ -35,6 +35,7 @@ sp-rpc = { version = "6.0.0", path = "../../primitives/rpc" }
sp-runtime = { version = "7.0.0", path = "../../primitives/runtime" }
sp-session = { version = "4.0.0-dev", path = "../../primitives/session" }
sp-version = { version = "5.0.0", path = "../../primitives/version" }
sp-statement-store = { version = "4.0.0-dev", path = "../../primitives/statement-store" }
tokio = "1.22.0"
+1
View File
@@ -36,6 +36,7 @@ pub mod chain;
pub mod dev;
pub mod offchain;
pub mod state;
pub mod statement;
pub mod system;
#[cfg(any(test, feature = "test-helpers"))]
+108
View File
@@ -0,0 +1,108 @@
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program 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.
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
//! Substrate statement store API.
use codec::{Decode, Encode};
use jsonrpsee::core::{async_trait, RpcResult};
/// Re-export the API for backward compatibility.
pub use sc_rpc_api::statement::{error::Error, StatementApiServer};
use sc_rpc_api::DenyUnsafe;
use sp_core::Bytes;
use sp_statement_store::{StatementSource, SubmitResult};
use std::sync::Arc;
/// Statement store API
pub struct StatementStore {
store: Arc<dyn sp_statement_store::StatementStore>,
deny_unsafe: DenyUnsafe,
}
impl StatementStore {
/// Create new instance of Offchain API.
pub fn new(
store: Arc<dyn sp_statement_store::StatementStore>,
deny_unsafe: DenyUnsafe,
) -> Self {
StatementStore { store, deny_unsafe }
}
}
#[async_trait]
impl StatementApiServer for StatementStore {
fn dump(&self) -> RpcResult<Vec<Bytes>> {
self.deny_unsafe.check_if_safe()?;
let statements =
self.store.statements().map_err(|e| Error::StatementStore(e.to_string()))?;
Ok(statements.into_iter().map(|(_, s)| s.encode().into()).collect())
}
fn broadcasts(&self, match_all_topics: Vec<[u8; 32]>) -> RpcResult<Vec<Bytes>> {
Ok(self
.store
.broadcasts(&match_all_topics)
.map_err(|e| Error::StatementStore(e.to_string()))?
.into_iter()
.map(Into::into)
.collect())
}
fn posted(&self, match_all_topics: Vec<[u8; 32]>, dest: [u8; 32]) -> RpcResult<Vec<Bytes>> {
Ok(self
.store
.posted(&match_all_topics, dest)
.map_err(|e| Error::StatementStore(e.to_string()))?
.into_iter()
.map(Into::into)
.collect())
}
fn posted_clear(
&self,
match_all_topics: Vec<[u8; 32]>,
dest: [u8; 32],
) -> RpcResult<Vec<Bytes>> {
Ok(self
.store
.posted_clear(&match_all_topics, dest)
.map_err(|e| Error::StatementStore(e.to_string()))?
.into_iter()
.map(Into::into)
.collect())
}
fn submit(&self, encoded: Bytes) -> RpcResult<()> {
let statement = Decode::decode(&mut &*encoded)
.map_err(|e| Error::StatementStore(format!("Eror decoding statement: {:?}", e)))?;
match self.store.submit(statement, StatementSource::Local) {
SubmitResult::New(_) | SubmitResult::Known => Ok(()),
// `KnownExpired` should not happen. Expired statements submitted with
// `StatementSource::Rpc` should be renewed.
SubmitResult::KnownExpired =>
Err(Error::StatementStore("Submitted an expired statement.".into()).into()),
SubmitResult::Bad(e) => Err(Error::StatementStore(e.into()).into()),
SubmitResult::Ignored => Err(Error::StatementStore("Store is full.".into()).into()),
SubmitResult::InternalError(e) => Err(Error::StatementStore(e.to_string()).into()),
}
}
fn remove(&self, hash: [u8; 32]) -> RpcResult<()> {
Ok(self.store.remove(&hash).map_err(|e| Error::StatementStore(e.to_string()))?)
}
}
+1 -1
View File
@@ -938,8 +938,8 @@ where
Arc::new(TransactionPoolAdapter { pool: transaction_pool, client: client.clone() }),
config.prometheus_config.as_ref().map(|config| &config.registry),
)?;
spawn_handle.spawn("network-transactions-handler", Some("networking"), tx_handler.run());
spawn_handle.spawn_blocking(
"chain-sync-network-service-provider",
Some("networking"),
+4 -2
View File
@@ -130,8 +130,10 @@ pub struct Configuration {
pub max_runtime_instances: usize,
/// Announce block automatically after they have been imported
pub announce_block: bool,
/// Base path of the configuration
pub base_path: Option<BasePath>,
/// Data path root for the configured chain.
pub data_path: PathBuf,
/// Base path of the configuration. This is shared between chains.
pub base_path: BasePath,
/// Configuration of the output format that the informant uses.
pub informant_output_format: sc_informant::OutputFormat,
/// Maximum number of different runtime versions that can be cached.
+2 -1
View File
@@ -265,7 +265,8 @@ fn node_config<
tracing_receiver: Default::default(),
max_runtime_instances: 8,
announce_block: true,
base_path: Some(BasePath::new(root)),
base_path: BasePath::new(root.clone()),
data_path: root,
informant_output_format: Default::default(),
runtime_cache_size: 2,
}
@@ -0,0 +1,36 @@
[package]
name = "sc-statement-store"
version = "4.0.0-dev"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2021"
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage = "https://substrate.io"
repository = "https://github.com/paritytech/substrate/"
description = "Substrate statement store."
readme = "README.md"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
async-trait = "0.1.57"
codec = { package = "parity-scale-codec", version = "3.2.2" }
futures = "0.3.21"
futures-timer = "3.0.2"
log = "0.4.17"
parking_lot = "0.12.1"
parity-db = "0.4.6"
tokio = { version = "1.22.0", features = ["time"] }
sp-statement-store = { version = "4.0.0-dev", path = "../../primitives/statement-store" }
prometheus-endpoint = { package = "substrate-prometheus-endpoint", version = "0.10.0-dev", path = "../../utils/prometheus" }
sp-api = { version = "4.0.0-dev", path = "../../primitives/api" }
sp-blockchain = { version = "4.0.0-dev", path = "../../primitives/blockchain" }
sp-core = { version = "7.0.0", path = "../../primitives/core" }
sp-runtime = { version = "7.0.0", path = "../../primitives/runtime" }
sp-tracing = { version = "6.0.0", path = "../../primitives/tracing" }
sc-client-api = { version = "4.0.0-dev", path = "../api" }
[dev-dependencies]
tempfile = "3.1.0"
env_logger = "0.9"
@@ -0,0 +1,4 @@
Substrate statement store implementation.
License: GPL-3.0-or-later WITH Classpath-exception-2.0
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,79 @@
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program 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.
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
//! Statement store Prometheus metrics.
use std::sync::Arc;
use prometheus_endpoint::{register, Counter, PrometheusError, Registry, U64};
#[derive(Clone, Default)]
pub struct MetricsLink(Arc<Option<Metrics>>);
impl MetricsLink {
pub fn new(registry: Option<&Registry>) -> Self {
Self(Arc::new(registry.and_then(|registry| {
Metrics::register(registry)
.map_err(|err| {
log::warn!("Failed to register prometheus metrics: {}", err);
})
.ok()
})))
}
pub fn report(&self, do_this: impl FnOnce(&Metrics)) {
if let Some(metrics) = self.0.as_ref() {
do_this(metrics);
}
}
}
/// Statement store Prometheus metrics.
pub struct Metrics {
pub submitted_statements: Counter<U64>,
pub validations_invalid: Counter<U64>,
pub statements_pruned: Counter<U64>,
}
impl Metrics {
pub fn register(registry: &Registry) -> Result<Self, PrometheusError> {
Ok(Self {
submitted_statements: register(
Counter::new(
"substrate_sub_statement_store_submitted_statements",
"Total number of statements submitted",
)?,
registry,
)?,
validations_invalid: register(
Counter::new(
"substrate_sub_statement_store_validations_invalid",
"Total number of statements that were removed from the pool as invalid",
)?,
registry,
)?,
statements_pruned: register(
Counter::new(
"substrate_sub_statement_store_block_statements",
"Total number of statements that was requested to be pruned by block events",
)?,
registry,
)?,
})
}
}
+46
View File
@@ -0,0 +1,46 @@
[package]
name = "pallet-statement"
version = "4.0.0-dev"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2021"
license = "Apache-2.0"
homepage = "https://substrate.io"
repository = "https://github.com/paritytech/substrate/"
description = "FRAME pallet for statement store"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = ["derive"]}
scale-info = { version = "2.1.1", default-features = false, features = ["derive"] }
frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" }
frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" }
sp-statement-store = { version = "4.0.0-dev", default-features = false, path = "../../primitives/statement-store" }
sp-api = { version = "4.0.0-dev", default-features = false, path = "../../primitives/api" }
sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime" }
sp-std = { version = "5.0.0", default-features = false, path = "../../primitives/std" }
sp-io = { version = "7.0.0", default-features = false, path = "../../primitives/io" }
sp-core = { version = "7.0.0", default-features = false, path = "../../primitives/core" }
log = { version = "0.4.17", default-features = false }
[dev-dependencies]
pallet-balances = { version = "4.0.0-dev", path = "../balances" }
[features]
default = [ "std" ]
std = [
"codec/std",
"scale-info/std",
"frame-support/std",
"frame-system/std",
"sp-api/std",
"sp-runtime/std",
"sp-std/std",
"sp-io/std",
"sp-core/std",
"sp-statement-store/std",
]
try-runtime = [
"frame-support/try-runtime",
]
+222
View File
@@ -0,0 +1,222 @@
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Supporting pallet for the statement store.
//!
//! - [`Pallet`]
//!
//! ## Overview
//!
//! The Statement pallet provides means to create and validate statements for the statement store.
//!
//! For each statement validation function calculates the following three values based on the
//! statement author balance:
//! `max_count`: Maximum number of statements allowed for the author (signer) of this statement.
//! `max_size`: Maximum total size of statements allowed for the author (signer) of this statement.
//!
//! This pallet also contains an offchain worker that turns on-chain statement events into
//! statements. These statements are placed in the store and propagated over the network.
#![cfg_attr(not(feature = "std"), no_std)]
use frame_support::{
pallet_prelude::*,
sp_runtime::{traits::CheckedDiv, SaturatedConversion},
traits::fungible::Inspect,
};
use frame_system::pallet_prelude::*;
use sp_statement_store::{
runtime_api::{InvalidStatement, StatementSource, ValidStatement},
Proof, SignatureVerificationResult, Statement,
};
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
pub use pallet::*;
const LOG_TARGET: &str = "runtime::statement";
#[frame_support::pallet]
pub mod pallet {
use super::*;
pub type BalanceOf<T> =
<<T as Config>::Currency as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
#[pallet::config]
pub trait Config: frame_system::Config
where
<Self as frame_system::Config>::AccountId: From<sp_statement_store::AccountId>,
{
/// The overarching event type.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// The currency which is used to calculate account limits.
type Currency: Inspect<Self::AccountId>;
/// Min balance for priority statements.
#[pallet::constant]
type StatementCost: Get<BalanceOf<Self>>;
/// Cost of data byte used for priority calculation.
#[pallet::constant]
type ByteCost: Get<BalanceOf<Self>>;
/// Minimum number of statements allowed per account.
#[pallet::constant]
type MinAllowedStatements: Get<u32>;
/// Maximum number of statements allowed per account.
#[pallet::constant]
type MaxAllowedStatements: Get<u32>;
/// Minimum data bytes allowed per account.
#[pallet::constant]
type MinAllowedBytes: Get<u32>;
/// Maximum data bytes allowed per account.
#[pallet::constant]
type MaxAllowedBytes: Get<u32>;
}
#[pallet::pallet]
pub struct Pallet<T>(sp_std::marker::PhantomData<T>);
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config>
where
<T as frame_system::Config>::AccountId: From<sp_statement_store::AccountId>,
{
/// A new statement is submitted
NewStatement { account: T::AccountId, statement: Statement },
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T>
where
<T as frame_system::Config>::AccountId: From<sp_statement_store::AccountId>,
sp_statement_store::AccountId: From<<T as frame_system::Config>::AccountId>,
<T as frame_system::Config>::RuntimeEvent: From<pallet::Event<T>>,
<T as frame_system::Config>::RuntimeEvent: TryInto<pallet::Event<T>>,
sp_statement_store::BlockHash: From<<T as frame_system::Config>::Hash>,
{
fn offchain_worker(now: BlockNumberFor<T>) {
log::trace!(target: LOG_TARGET, "Collecting statements at #{:?}", now);
Pallet::<T>::collect_statements();
}
}
}
impl<T: Config> Pallet<T>
where
<T as frame_system::Config>::AccountId: From<sp_statement_store::AccountId>,
sp_statement_store::AccountId: From<<T as frame_system::Config>::AccountId>,
<T as frame_system::Config>::RuntimeEvent: From<pallet::Event<T>>,
<T as frame_system::Config>::RuntimeEvent: TryInto<pallet::Event<T>>,
sp_statement_store::BlockHash: From<<T as frame_system::Config>::Hash>,
{
/// Validate a statement against current state. This is supposed to be called by the statement
/// store on the host side.
pub fn validate_statement(
_source: StatementSource,
mut statement: Statement,
) -> Result<ValidStatement, InvalidStatement> {
sp_io::init_tracing();
log::debug!(target: LOG_TARGET, "Validating statement {:?}", statement);
let account: T::AccountId = match statement.proof() {
Some(Proof::OnChain { who, block_hash, event_index }) => {
if frame_system::Pallet::<T>::parent_hash().as_ref() != block_hash.as_slice() {
log::debug!(target: LOG_TARGET, "Bad block hash.");
return Err(InvalidStatement::BadProof)
}
let account: T::AccountId = (*who).into();
match frame_system::Pallet::<T>::event_no_consensus(*event_index as usize) {
Some(e) => {
statement.remove_proof();
if let Ok(Event::NewStatement { account: a, statement: s }) = e.try_into() {
if a != account || s != statement {
log::debug!(target: LOG_TARGET, "Event data mismatch");
return Err(InvalidStatement::BadProof)
}
} else {
log::debug!(target: LOG_TARGET, "Event type mismatch");
return Err(InvalidStatement::BadProof)
}
},
_ => {
log::debug!(target: LOG_TARGET, "Bad event index");
return Err(InvalidStatement::BadProof)
},
}
account
},
_ => match statement.verify_signature() {
SignatureVerificationResult::Valid(account) => account.into(),
SignatureVerificationResult::Invalid => {
log::debug!(target: LOG_TARGET, "Bad statement signature.");
return Err(InvalidStatement::BadProof)
},
SignatureVerificationResult::NoSignature => {
log::debug!(target: LOG_TARGET, "Missing statement signature.");
return Err(InvalidStatement::NoProof)
},
},
};
let statement_cost = T::StatementCost::get();
let byte_cost = T::ByteCost::get();
let balance = <T::Currency as Inspect<AccountIdOf<T>>>::balance(&account);
let min_allowed_statements = T::MinAllowedStatements::get();
let max_allowed_statements = T::MaxAllowedStatements::get();
let min_allowed_bytes = T::MinAllowedBytes::get();
let max_allowed_bytes = T::MaxAllowedBytes::get();
let max_count = balance
.checked_div(&statement_cost)
.unwrap_or_default()
.saturated_into::<u32>()
.clamp(min_allowed_statements, max_allowed_statements);
let max_size = balance
.checked_div(&byte_cost)
.unwrap_or_default()
.saturated_into::<u32>()
.clamp(min_allowed_bytes, max_allowed_bytes);
Ok(ValidStatement { max_count, max_size })
}
/// Submit a statement event. The statement will be picked up by the offchain worker and
/// broadcast to the network.
pub fn submit_statement(account: T::AccountId, statement: Statement) {
Self::deposit_event(Event::NewStatement { account, statement });
}
fn collect_statements() {
// Find `NewStatement` events and submit them to the store
for (index, event) in frame_system::Pallet::<T>::read_events_no_consensus().enumerate() {
if let Ok(Event::<T>::NewStatement { account, mut statement }) = event.event.try_into()
{
if statement.proof().is_none() {
let proof = Proof::OnChain {
who: account.into(),
block_hash: frame_system::Pallet::<T>::parent_hash().into(),
event_index: index as u64,
};
statement.set_proof(proof);
}
sp_statement_store::runtime_api::statement_store::submit_statement(statement);
}
}
}
}
+126
View File
@@ -0,0 +1,126 @@
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! # Statement pallet test environment.
use super::*;
use crate as pallet_statement;
use frame_support::{
ord_parameter_types,
traits::{ConstU32, ConstU64, Everything},
weights::constants::RocksDbWeight,
};
use sp_core::{Pair, H256};
use sp_runtime::{
testing::Header,
traits::{BlakeTwo256, IdentityLookup},
AccountId32,
};
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
type Block = frame_system::mocking::MockBlock<Test>;
pub const MIN_ALLOWED_STATEMENTS: u32 = 4;
pub const MAX_ALLOWED_STATEMENTS: u32 = 10;
pub const MIN_ALLOWED_BYTES: u32 = 1024;
pub const MAX_ALLOWED_BYTES: u32 = 4096;
frame_support::construct_runtime!(
pub enum Test where
Block = Block,
NodeBlock = Block,
UncheckedExtrinsic = UncheckedExtrinsic,
{
System: frame_system,
Balances: pallet_balances,
Statement: pallet_statement,
}
);
impl frame_system::Config for Test {
type BaseCallFilter = Everything;
type BlockWeights = ();
type BlockLength = ();
type DbWeight = RocksDbWeight;
type RuntimeOrigin = RuntimeOrigin;
type RuntimeCall = RuntimeCall;
type Index = u64;
type BlockNumber = u64;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = AccountId32;
type Lookup = IdentityLookup<Self::AccountId>;
type Header = Header;
type RuntimeEvent = RuntimeEvent;
type BlockHashCount = ConstU64<250>;
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = pallet_balances::AccountData<u64>;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ();
type OnSetCode = ();
type MaxConsumers = ConstU32<16>;
}
impl pallet_balances::Config for Test {
type Balance = u64;
type RuntimeEvent = RuntimeEvent;
type DustRemoval = ();
type ExistentialDeposit = ConstU64<5>;
type AccountStore = System;
type WeightInfo = ();
type MaxLocks = ();
type MaxReserves = ConstU32<50>;
type ReserveIdentifier = [u8; 8];
type FreezeIdentifier = ();
type MaxFreezes = ();
type HoldIdentifier = ();
type MaxHolds = ();
}
ord_parameter_types! {
pub const One: u64 = 1;
}
impl Config for Test {
type RuntimeEvent = RuntimeEvent;
type Currency = Balances;
type StatementCost = ConstU64<1000>;
type ByteCost = ConstU64<2>;
type MinAllowedStatements = ConstU32<MIN_ALLOWED_STATEMENTS>;
type MaxAllowedStatements = ConstU32<MAX_ALLOWED_STATEMENTS>;
type MinAllowedBytes = ConstU32<MIN_ALLOWED_BYTES>;
type MaxAllowedBytes = ConstU32<MAX_ALLOWED_BYTES>;
}
pub fn new_test_ext() -> sp_io::TestExternalities {
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
let balances = pallet_balances::GenesisConfig::<Test> {
balances: vec![
(sp_core::sr25519::Pair::from_string("//Alice", None).unwrap().public().into(), 6000),
(
sp_core::sr25519::Pair::from_string("//Charlie", None).unwrap().public().into(),
500000,
),
],
};
balances.assimilate_storage(&mut t).unwrap();
t.into()
}
+159
View File
@@ -0,0 +1,159 @@
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! # Statement runtime support tests.
#![cfg(test)]
use super::*;
use crate::mock::*;
use sp_core::Pair;
use sp_runtime::AccountId32;
use sp_statement_store::{
runtime_api::{InvalidStatement, StatementSource, ValidStatement},
Proof, Statement,
};
#[test]
fn sign_and_validate_no_balance() {
new_test_ext().execute_with(|| {
let pair = sp_core::sr25519::Pair::from_string("//Bob", None).unwrap();
let mut statement = Statement::new();
statement.sign_sr25519_private(&pair);
let result = Pallet::<Test>::validate_statement(StatementSource::Chain, statement);
assert_eq!(
Ok(ValidStatement { max_count: MIN_ALLOWED_STATEMENTS, max_size: MIN_ALLOWED_BYTES }),
result
);
let pair = sp_core::ed25519::Pair::from_string("//Bob", None).unwrap();
let mut statement = Statement::new();
statement.sign_ed25519_private(&pair);
let result = Pallet::<Test>::validate_statement(StatementSource::Chain, statement);
assert_eq!(
Ok(ValidStatement { max_count: MIN_ALLOWED_STATEMENTS, max_size: MIN_ALLOWED_BYTES }),
result
);
let pair = sp_core::ecdsa::Pair::from_string("//Bob", None).unwrap();
let mut statement = Statement::new();
statement.sign_ecdsa_private(&pair);
let result = Pallet::<Test>::validate_statement(StatementSource::Chain, statement);
assert_eq!(
Ok(ValidStatement { max_count: MIN_ALLOWED_STATEMENTS, max_size: MIN_ALLOWED_BYTES }),
result
);
});
}
#[test]
fn validate_with_balance() {
new_test_ext().execute_with(|| {
let pair = sp_core::sr25519::Pair::from_string("//Alice", None).unwrap();
let mut statement = Statement::new();
statement.sign_sr25519_private(&pair);
let result = Pallet::<Test>::validate_statement(StatementSource::Chain, statement);
assert_eq!(Ok(ValidStatement { max_count: 6, max_size: 3000 }), result);
let pair = sp_core::sr25519::Pair::from_string("//Charlie", None).unwrap();
let mut statement = Statement::new();
statement.sign_sr25519_private(&pair);
let result = Pallet::<Test>::validate_statement(StatementSource::Chain, statement);
assert_eq!(
Ok(ValidStatement { max_count: MAX_ALLOWED_STATEMENTS, max_size: MAX_ALLOWED_BYTES }),
result
);
});
}
#[test]
fn validate_no_proof_fails() {
new_test_ext().execute_with(|| {
let statement = Statement::new();
let result = Pallet::<Test>::validate_statement(StatementSource::Chain, statement);
assert_eq!(Err(InvalidStatement::NoProof), result);
});
}
#[test]
fn validate_bad_signature_fails() {
new_test_ext().execute_with(|| {
let statement = Statement::new_with_proof(Proof::Sr25519 {
signature: [0u8; 64],
signer: Default::default(),
});
let result = Pallet::<Test>::validate_statement(StatementSource::Chain, statement);
assert_eq!(Err(InvalidStatement::BadProof), result);
});
}
#[test]
fn validate_event() {
new_test_ext().execute_with(|| {
let parent_hash = sp_core::H256::random();
System::reset_events();
System::initialize(&1, &parent_hash, &Default::default());
let mut statement = Statement::new();
let pair = sp_core::sr25519::Pair::from_string("//Alice", None).unwrap();
let account: AccountId32 = pair.public().into();
Pallet::<Test>::submit_statement(account.clone(), statement.clone());
statement.set_proof(Proof::OnChain {
who: account.clone().into(),
event_index: 0,
block_hash: parent_hash.into(),
});
let result = Pallet::<Test>::validate_statement(StatementSource::Chain, statement.clone());
assert_eq!(Ok(ValidStatement { max_count: 6, max_size: 3000 }), result);
// Use wrong event index
statement.set_proof(Proof::OnChain {
who: account.clone().into(),
event_index: 1,
block_hash: parent_hash.into(),
});
let result = Pallet::<Test>::validate_statement(StatementSource::Chain, statement.clone());
assert_eq!(Err(InvalidStatement::BadProof), result);
// Use wrong block hash
statement.set_proof(Proof::OnChain {
who: account.clone().into(),
event_index: 0,
block_hash: sp_core::H256::random().into(),
});
let result = Pallet::<Test>::validate_statement(StatementSource::Chain, statement.clone());
assert_eq!(Err(InvalidStatement::BadProof), result);
});
}
#[test]
fn validate_no_event_fails() {
new_test_ext().execute_with(|| {
let parent_hash = sp_core::H256::random();
System::reset_events();
System::initialize(&1, &parent_hash, &Default::default());
let mut statement = Statement::new();
let pair = sp_core::sr25519::Pair::from_string("//Alice", None).unwrap();
let account: AccountId32 = pair.public().into();
statement.set_proof(Proof::OnChain {
who: account.into(),
event_index: 0,
block_hash: parent_hash.into(),
});
let result = Pallet::<Test>::validate_statement(StatementSource::Chain, statement);
assert_eq!(Err(InvalidStatement::BadProof), result);
});
}
+8
View File
@@ -1452,6 +1452,14 @@ impl<T: Config> Pallet<T> {
Self::read_events_no_consensus().map(|e| *e).collect()
}
/// Get a single event at specified index.
///
/// Should only be called if you know what you are doing and outside of the runtime block
/// execution else it can have a large impact on the PoV size of a block.
pub fn event_no_consensus(index: usize) -> Option<T::RuntimeEvent> {
Self::read_events_no_consensus().nth(index).map(|e| e.event.clone())
}
/// Get the current events deposited by the runtime.
///
/// Should only be called if you know what you are doing and outside of the runtime block
@@ -309,6 +309,13 @@ macro_rules! app_crypto_public_common {
<$public>::try_from(data).map(Into::into)
}
}
impl Public {
/// Convert into wrapped generic public key type.
pub fn into_inner(self) -> $public {
self.0
}
}
};
}
@@ -470,6 +477,13 @@ macro_rules! app_crypto_signature_common {
Self::try_from(&data[..])
}
}
impl Signature {
/// Convert into wrapped generic signature type.
pub fn into_inner(self) -> $sig {
self.0
}
}
};
}
@@ -159,6 +159,9 @@ pub enum Error {
#[error("State Database error: {0}")]
StateDatabase(String),
#[error("Statement store error: {0}")]
StatementStore(String),
#[error("Failed to set the chain head to a block that's too old.")]
SetHeadTooOld,
+2
View File
@@ -1140,6 +1140,8 @@ pub mod key_types {
pub const AUTHORITY_DISCOVERY: KeyTypeId = KeyTypeId(*b"audi");
/// Key type for staking, built-in. Identified as `stak`.
pub const STAKING: KeyTypeId = KeyTypeId(*b"stak");
/// A key type for signing statements
pub const STATEMENT: KeyTypeId = KeyTypeId(*b"stmt");
/// A key type ID useful for tests.
pub const DUMMY: KeyTypeId = KeyTypeId(*b"dumy");
}
+2 -10
View File
@@ -278,16 +278,8 @@ bitflags::bitflags! {
const NODE_AUTHORIZATION = 0b0000_1000_0000;
/// Access time related functionality
const TIME = 0b0001_0000_0000;
}
}
impl Capabilities {
/// Return capabilities for rich offchain calls.
///
/// Those calls should be allowed to sign and submit transactions
/// and access offchain workers database (but read only!).
pub fn rich_offchain_call() -> Self {
Capabilities::TRANSACTION_POOL | Capabilities::KEYSTORE | Capabilities::OFFCHAIN_DB_READ
/// Access the statement store.
const STATEMENT_STORE = 0b0010_0000_0000;
}
}
@@ -0,0 +1,41 @@
[package]
name = "sp-statement-store"
version = "4.0.0-dev"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2021"
license = "Apache-2.0"
homepage = "https://substrate.io"
repository = "https://github.com/paritytech/substrate/"
description = "A crate which contains primitives related to the statement store"
readme = "README.md"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = ["derive"] }
scale-info = { version = "2.1.1", default-features = false, features = ["derive"] }
sp-core = { version = "7.0.0", default-features = false, path = "../core" }
sp-runtime = { version = "7.0.0", default-features = false, path = "../runtime" }
sp-std = { version = "5.0.0", default-features = false, path = "../std" }
sp-api = { version = "4.0.0-dev", default-features = false, path = "../api" }
sp-application-crypto = { version = "7.0.0", default-features = false, path = "../application-crypto" }
sp-runtime-interface = { version = "7.0.0", default-features = false, path = "../runtime-interface" }
sp-externalities = { version = "0.13.0", default-features = false, path = "../externalities" }
thiserror = { version = "1.0", optional = true }
log = { version = "0.4.17", optional = true }
[features]
default = ["std"]
std = [
"codec/std",
"scale-info/std",
"sp-core/std",
"sp-runtime/std",
"sp-runtime-interface/std",
"sp-std/std",
"sp-api/std",
"sp-application-crypto/std",
"thiserror",
"log",
]
@@ -0,0 +1,39 @@
Statement store is an off-chain data-store for signed statements accessible via RPC and OCW.
Nodes hold a number of statements with a proof of authenticity owing to an account ID. OCWs can place items in the data-store (with valid signatures) for any accounts whose keys they control. Users can also submit pre-signed statements via RPC. Statements can also be submitted from on-chain logic through an on-chain event.
A new system event `NewStatement` is added to the runtime. This event allows any account on-chain to declare that they want to make a statement for the store. Within the node store and for broadcasting, the statement would be accompanied with the hash of the block and index of the event within it, essentially taking the place of a real signature.
Statements comprise an optional proof of authenticity (e.g. a signature) and a number of fields. For statements without a proof, nodes would gossip statements randomly with a rate-limiter to minimise the chance of being overrun by a misbehaving node. These will generally be disregarded by nodes unless they are gossiped by several different peers or if a peer pays for it somehow (e.g. gossiping something valuable).
Each field is effectively a key/value pair. Fields must be sorted and the same field type may not be repeated. Depending on which keys are present, clients may index the message for ease of retrieval.
Formally, `Statement` is equivalent to the type `Vec<Field>` and `Field` is the SCALE-encoded enumeration:
- 0: `AuthenticityProof(Proof)`: The signature of the message. For cryptography where the public key cannot be derived from the signature together with the message data, then this will also include the signer's public key. The message data is all fields of the messages fields except the signature concatenated together *without the length prefix that a `Vec` would usually imply*. This is so that the signature can be derived without needing to re-encode the statement.
- 1: `DecryptionKey([u8; 32])`: The decryption key identifier which should be used to decrypt the statement's data. In the absense of this field `Data` should be treated as not encrypted.
- 2: `Priority(u32)`: Priority specifier. Higher priority statements should be kept around at the cost of lower priority statements if multiple statements from the same sender are competing for persistence or transport. Nodes should disregard when considering unsigned statements.
- 3: `Channel([u8; 32])`: The channel identifier. Only one message of a given channel should be retained at once (the one of highest priority). Nodes should disregard when considering unsigned statements.
- 4: `Topic1([u8; 32]))`: First topic identifier.
- 5: `Topic2([u8; 32]))`: Second topic identifier.
- 6: `Topic3([u8; 32]))`: Third topic identifier.
- 7: `Topic4([u8; 32]))`: Fourth topic identifier.
- 8: `Data(Vec<u8>)`: General data segment. No special meaning.
`Proof` is defined as the SCALE-encoded enumeration:
- 0: `Sr25519 { signature: [u8; 64], signer: [u8; 32] }`
- 1: `Ed25519 { signature: [u8; 64], signer: [u8; 32] )`
- 2: `Secp256k1Ecdsa { signature: [u8; 65], signer: [u8; 33] )`
- 3: `OnChain { who: [u8; 32], block_hash: [u8; 32], event_index: u64 }`
### Potential uses
Potential use-cases are various and include:
- ring-signature aggregation;
- messaging;
- state-channels;
- deferral of the initial "advertising" phase of multi-party transactions;
- publication of preimage data whose hash is referenced on-chain;
- effective transferal of fee payment to a second-party.
License: Apache-2.0
@@ -0,0 +1,618 @@
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#![cfg_attr(not(feature = "std"), no_std)]
#![warn(missing_docs)]
//! A crate which contains statement-store primitives.
use codec::{Decode, Encode};
use scale_info::TypeInfo;
use sp_application_crypto::RuntimeAppPublic;
#[cfg(feature = "std")]
use sp_core::Pair;
use sp_runtime_interface::pass_by::PassByCodec;
use sp_std::vec::Vec;
/// Statement topic.
pub type Topic = [u8; 32];
/// Decryption key identifier.
pub type DecryptionKey = [u8; 32];
/// Statement hash.
pub type Hash = [u8; 32];
/// Block hash.
pub type BlockHash = [u8; 32];
/// Account id
pub type AccountId = [u8; 32];
/// Statement channel.
pub type Channel = [u8; 32];
/// Total number of topic fields allowed.
pub const MAX_TOPICS: usize = 4;
#[cfg(feature = "std")]
pub use store_api::{
Error, NetworkPriority, Result, StatementSource, StatementStore, SubmitResult,
};
pub mod runtime_api;
#[cfg(feature = "std")]
mod store_api;
mod sr25519 {
mod app_sr25519 {
use sp_application_crypto::{app_crypto, key_types::STATEMENT, sr25519};
app_crypto!(sr25519, STATEMENT);
}
pub type Public = app_sr25519::Public;
}
mod ed25519 {
mod app_ed25519 {
use sp_application_crypto::{app_crypto, ed25519, key_types::STATEMENT};
app_crypto!(ed25519, STATEMENT);
}
pub type Public = app_ed25519::Public;
}
mod ecdsa {
mod app_ecdsa {
use sp_application_crypto::{app_crypto, ecdsa, key_types::STATEMENT};
app_crypto!(ecdsa, STATEMENT);
}
pub type Public = app_ecdsa::Public;
}
/// Returns blake2-256 hash for the encoded statement.
#[cfg(feature = "std")]
pub fn hash_encoded(data: &[u8]) -> [u8; 32] {
sp_core::hashing::blake2_256(data)
}
/// Statement proof.
#[derive(Encode, Decode, TypeInfo, sp_core::RuntimeDebug, Clone, PartialEq, Eq)]
pub enum Proof {
/// Sr25519 Signature.
Sr25519 {
/// Signature.
signature: [u8; 64],
/// Public key.
signer: [u8; 32],
},
/// Ed25519 Signature.
Ed25519 {
/// Signature.
signature: [u8; 64],
/// Public key.
signer: [u8; 32],
},
/// Secp256k1 Signature.
Secp256k1Ecdsa {
/// Signature.
signature: [u8; 65],
/// Public key.
signer: [u8; 33],
},
/// On-chain event proof.
OnChain {
/// Account identifier associated with the event.
who: AccountId,
/// Hash of block that contains the event.
block_hash: BlockHash,
/// Index of the event in the event list.
event_index: u64,
},
}
impl Proof {
/// Return account id for the proof creator.
pub fn account_id(&self) -> AccountId {
match self {
Proof::Sr25519 { signer, .. } => *signer,
Proof::Ed25519 { signer, .. } => *signer,
Proof::Secp256k1Ecdsa { signer, .. } =>
<sp_runtime::traits::BlakeTwo256 as sp_core::Hasher>::hash(signer).into(),
Proof::OnChain { who, .. } => *who,
}
}
}
/// Statement attributes. Each statement is a list of 0 or more fields. Fields may only appear once
/// and in the order declared here.
#[derive(Encode, Decode, TypeInfo, sp_core::RuntimeDebug, Clone, PartialEq, Eq)]
#[repr(u8)]
pub enum Field {
/// Statement proof.
AuthenticityProof(Proof) = 0,
/// An identifier for the key that `Data` field may be decrypted with.
DecryptionKey(DecryptionKey) = 1,
/// Priority when competing with other messages from the same sender.
Priority(u32) = 2,
/// Account channel to use. Only one message per `(account, channel)` pair is allowed.
Channel(Channel) = 3,
/// First statement topic.
Topic1(Topic) = 4,
/// Second statement topic.
Topic2(Topic) = 5,
/// Third statement topic.
Topic3(Topic) = 6,
/// Fourth statement topic.
Topic4(Topic) = 7,
/// Additional data.
Data(Vec<u8>) = 8,
}
impl Field {
fn discriminant(&self) -> u8 {
// This is safe for repr(u8)
// see https://doc.rust-lang.org/reference/items/enumerations.html#pointer-casting
unsafe { *(self as *const Self as *const u8) }
}
}
/// Statement structure.
#[derive(TypeInfo, sp_core::RuntimeDebug, PassByCodec, Clone, PartialEq, Eq, Default)]
pub struct Statement {
proof: Option<Proof>,
decryption_key: Option<DecryptionKey>,
channel: Option<Channel>,
priority: Option<u32>,
num_topics: u8,
topics: [Topic; MAX_TOPICS],
data: Option<Vec<u8>>,
}
impl Decode for Statement {
fn decode<I: codec::Input>(input: &mut I) -> core::result::Result<Self, codec::Error> {
// Encoding matches that of Vec<Field>. Basically this just means accepting that there
// will be a prefix of vector length.
let num_fields: codec::Compact<u32> = Decode::decode(input)?;
let mut tag = 0;
let mut statement = Statement::new();
for i in 0..num_fields.into() {
let field: Field = Decode::decode(input)?;
if i > 0 && field.discriminant() <= tag {
return Err("Invalid field order or duplicate fields".into())
}
tag = field.discriminant();
match field {
Field::AuthenticityProof(p) => statement.set_proof(p),
Field::DecryptionKey(key) => statement.set_decryption_key(key),
Field::Priority(p) => statement.set_priority(p),
Field::Channel(c) => statement.set_channel(c),
Field::Topic1(t) => statement.set_topic(0, t),
Field::Topic2(t) => statement.set_topic(1, t),
Field::Topic3(t) => statement.set_topic(2, t),
Field::Topic4(t) => statement.set_topic(3, t),
Field::Data(data) => statement.set_plain_data(data),
}
}
Ok(statement)
}
}
impl Encode for Statement {
fn encode(&self) -> Vec<u8> {
self.encoded(false)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
/// Result returned by `Statement::verify_signature`
pub enum SignatureVerificationResult {
/// Signature is valid and matches this account id.
Valid(AccountId),
/// Signature has failed verification.
Invalid,
/// No signature in the proof or no proof.
NoSignature,
}
impl Statement {
/// Create a new empty statement with no proof.
pub fn new() -> Statement {
Default::default()
}
/// Create a new statement with a proof.
pub fn new_with_proof(proof: Proof) -> Statement {
let mut statement = Self::new();
statement.set_proof(proof);
statement
}
/// Sign with a key that matches given public key in the keystore.
///
/// Returns `true` if signing worked (private key present etc).
///
/// NOTE: This can only be called from the runtime.
pub fn sign_sr25519_public(&mut self, key: &sr25519::Public) -> bool {
let to_sign = self.signature_material();
if let Some(signature) = key.sign(&to_sign) {
let proof = Proof::Sr25519 {
signature: signature.into_inner().into(),
signer: key.clone().into_inner().into(),
};
self.set_proof(proof);
true
} else {
false
}
}
/// Sign with a given private key and add the signature proof field.
#[cfg(feature = "std")]
pub fn sign_sr25519_private(&mut self, key: &sp_core::sr25519::Pair) {
let to_sign = self.signature_material();
let proof =
Proof::Sr25519 { signature: key.sign(&to_sign).into(), signer: key.public().into() };
self.set_proof(proof);
}
/// Sign with a key that matches given public key in the keystore.
///
/// Returns `true` if signing worked (private key present etc).
///
/// NOTE: This can only be called from the runtime.
pub fn sign_ed25519_public(&mut self, key: &ed25519::Public) -> bool {
let to_sign = self.signature_material();
if let Some(signature) = key.sign(&to_sign) {
let proof = Proof::Ed25519 {
signature: signature.into_inner().into(),
signer: key.clone().into_inner().into(),
};
self.set_proof(proof);
true
} else {
false
}
}
/// Sign with a given private key and add the signature proof field.
#[cfg(feature = "std")]
pub fn sign_ed25519_private(&mut self, key: &sp_core::ed25519::Pair) {
let to_sign = self.signature_material();
let proof =
Proof::Ed25519 { signature: key.sign(&to_sign).into(), signer: key.public().into() };
self.set_proof(proof);
}
/// Sign with a key that matches given public key in the keystore.
///
/// Returns `true` if signing worked (private key present etc).
///
/// NOTE: This can only be called from the runtime.
///
/// Returns `true` if signing worked (private key present etc).
///
/// NOTE: This can only be called from the runtime.
pub fn sign_ecdsa_public(&mut self, key: &ecdsa::Public) -> bool {
let to_sign = self.signature_material();
if let Some(signature) = key.sign(&to_sign) {
let proof = Proof::Secp256k1Ecdsa {
signature: signature.into_inner().into(),
signer: key.clone().into_inner().0,
};
self.set_proof(proof);
true
} else {
false
}
}
/// Sign with a given private key and add the signature proof field.
#[cfg(feature = "std")]
pub fn sign_ecdsa_private(&mut self, key: &sp_core::ecdsa::Pair) {
let to_sign = self.signature_material();
let proof =
Proof::Secp256k1Ecdsa { signature: key.sign(&to_sign).into(), signer: key.public().0 };
self.set_proof(proof);
}
/// Check proof signature, if any.
pub fn verify_signature(&self) -> SignatureVerificationResult {
use sp_runtime::traits::Verify;
match self.proof() {
Some(Proof::OnChain { .. }) | None => SignatureVerificationResult::NoSignature,
Some(Proof::Sr25519 { signature, signer }) => {
let to_sign = self.signature_material();
let signature = sp_core::sr25519::Signature(*signature);
let public = sp_core::sr25519::Public(*signer);
if signature.verify(to_sign.as_slice(), &public) {
SignatureVerificationResult::Valid(*signer)
} else {
SignatureVerificationResult::Invalid
}
},
Some(Proof::Ed25519 { signature, signer }) => {
let to_sign = self.signature_material();
let signature = sp_core::ed25519::Signature(*signature);
let public = sp_core::ed25519::Public(*signer);
if signature.verify(to_sign.as_slice(), &public) {
SignatureVerificationResult::Valid(*signer)
} else {
SignatureVerificationResult::Invalid
}
},
Some(Proof::Secp256k1Ecdsa { signature, signer }) => {
let to_sign = self.signature_material();
let signature = sp_core::ecdsa::Signature(*signature);
let public = sp_core::ecdsa::Public(*signer);
if signature.verify(to_sign.as_slice(), &public) {
let sender_hash =
<sp_runtime::traits::BlakeTwo256 as sp_core::Hasher>::hash(signer);
SignatureVerificationResult::Valid(sender_hash.into())
} else {
SignatureVerificationResult::Invalid
}
},
}
}
/// Calculate statement hash.
#[cfg(feature = "std")]
pub fn hash(&self) -> [u8; 32] {
self.using_encoded(hash_encoded)
}
/// Returns a topic by topic index.
pub fn topic(&self, index: usize) -> Option<Topic> {
if index < self.num_topics as usize {
Some(self.topics[index])
} else {
None
}
}
/// Returns decryption key if any.
pub fn decryption_key(&self) -> Option<DecryptionKey> {
self.decryption_key
}
/// Convert to internal data.
pub fn into_data(self) -> Option<Vec<u8>> {
self.data
}
/// Get a reference to the statement proof, if any.
pub fn proof(&self) -> Option<&Proof> {
self.proof.as_ref()
}
/// Get proof account id, if any
pub fn account_id(&self) -> Option<AccountId> {
self.proof.as_ref().map(Proof::account_id)
}
/// Get plain data.
pub fn data(&self) -> Option<&Vec<u8>> {
self.data.as_ref()
}
/// Get plain data len.
pub fn data_len(&self) -> usize {
self.data().map_or(0, Vec::len)
}
/// Get channel, if any.
pub fn channel(&self) -> Option<Channel> {
self.channel
}
/// Get priority, if any.
pub fn priority(&self) -> Option<u32> {
self.priority
}
/// Return encoded fields that can be signed to construct or verify a proof
fn signature_material(&self) -> Vec<u8> {
self.encoded(true)
}
/// Remove the proof of this statement.
pub fn remove_proof(&mut self) {
self.proof = None;
}
/// Set statement proof. Any existing proof is overwritten.
pub fn set_proof(&mut self, proof: Proof) {
self.proof = Some(proof)
}
/// Set statement priority.
pub fn set_priority(&mut self, priority: u32) {
self.priority = Some(priority)
}
/// Set statement channel.
pub fn set_channel(&mut self, channel: Channel) {
self.channel = Some(channel)
}
/// Set topic by index. Does noting if index is over `MAX_TOPICS`.
pub fn set_topic(&mut self, index: usize, topic: Topic) {
if index < MAX_TOPICS {
self.topics[index] = topic;
self.num_topics = self.num_topics.max(index as u8 + 1);
}
}
/// Set decryption key.
pub fn set_decryption_key(&mut self, key: DecryptionKey) {
self.decryption_key = Some(key);
}
/// Set unencrypted statement data.
pub fn set_plain_data(&mut self, data: Vec<u8>) {
self.data = Some(data)
}
fn encoded(&self, for_signing: bool) -> Vec<u8> {
// Encoding matches that of Vec<Field>. Basically this just means accepting that there
// will be a prefix of vector length.
let num_fields = if !for_signing && self.proof.is_some() { 1 } else { 0 } +
if self.decryption_key.is_some() { 1 } else { 0 } +
if self.priority.is_some() { 1 } else { 0 } +
if self.channel.is_some() { 1 } else { 0 } +
if self.data.is_some() { 1 } else { 0 } +
self.num_topics as u32;
let mut output = Vec::new();
// When encoding signature payload, the length prefix is omitted.
// This is so that the signature for encoded statement can potentially be derived without
// needing to re-encode the statement.
if !for_signing {
let compact_len = codec::Compact::<u32>(num_fields);
compact_len.encode_to(&mut output);
if let Some(proof) = &self.proof {
0u8.encode_to(&mut output);
proof.encode_to(&mut output);
}
}
if let Some(decryption_key) = &self.decryption_key {
1u8.encode_to(&mut output);
decryption_key.encode_to(&mut output);
}
if let Some(priority) = &self.priority {
2u8.encode_to(&mut output);
priority.encode_to(&mut output);
}
if let Some(channel) = &self.channel {
3u8.encode_to(&mut output);
channel.encode_to(&mut output);
}
for t in 0..self.num_topics {
(4u8 + t).encode_to(&mut output);
self.topics[t as usize].encode_to(&mut output);
}
if let Some(data) = &self.data {
8u8.encode_to(&mut output);
data.encode_to(&mut output);
}
output
}
}
#[cfg(test)]
mod test {
use crate::{hash_encoded, Field, Proof, SignatureVerificationResult, Statement};
use codec::{Decode, Encode};
use sp_application_crypto::Pair;
#[test]
fn statement_encoding_matches_vec() {
let mut statement = Statement::new();
assert!(statement.proof().is_none());
let proof = Proof::OnChain { who: [42u8; 32], block_hash: [24u8; 32], event_index: 66 };
let decryption_key = [0xde; 32];
let topic1 = [0x01; 32];
let topic2 = [0x02; 32];
let data = vec![55, 99];
let priority = 999;
let channel = [0xcc; 32];
statement.set_proof(proof.clone());
statement.set_decryption_key(decryption_key);
statement.set_priority(priority);
statement.set_channel(channel);
statement.set_topic(0, topic1);
statement.set_topic(1, topic2);
statement.set_plain_data(data.clone());
statement.set_topic(5, [0x55; 32]);
assert_eq!(statement.topic(5), None);
let fields = vec![
Field::AuthenticityProof(proof.clone()),
Field::DecryptionKey(decryption_key),
Field::Priority(priority),
Field::Channel(channel),
Field::Topic1(topic1),
Field::Topic2(topic2),
Field::Data(data.clone()),
];
let encoded = statement.encode();
assert_eq!(statement.hash(), hash_encoded(&encoded));
assert_eq!(encoded, fields.encode());
let decoded = Statement::decode(&mut encoded.as_slice()).unwrap();
assert_eq!(decoded, statement);
}
#[test]
fn decode_checks_fields() {
let topic1 = [0x01; 32];
let topic2 = [0x02; 32];
let priority = 999;
let fields = vec![
Field::Priority(priority),
Field::Topic1(topic1),
Field::Topic1(topic1),
Field::Topic2(topic2),
]
.encode();
assert!(Statement::decode(&mut fields.as_slice()).is_err());
let fields =
vec![Field::Topic1(topic1), Field::Priority(priority), Field::Topic2(topic2)].encode();
assert!(Statement::decode(&mut fields.as_slice()).is_err());
}
#[test]
fn sign_and_verify() {
let mut statement = Statement::new();
statement.set_plain_data(vec![42]);
let sr25519_kp = sp_core::sr25519::Pair::from_string("//Alice", None).unwrap();
let ed25519_kp = sp_core::ed25519::Pair::from_string("//Alice", None).unwrap();
let secp256k1_kp = sp_core::ecdsa::Pair::from_string("//Alice", None).unwrap();
statement.sign_sr25519_private(&sr25519_kp);
assert_eq!(
statement.verify_signature(),
SignatureVerificationResult::Valid(sr25519_kp.public().0)
);
statement.sign_ed25519_private(&ed25519_kp);
assert_eq!(
statement.verify_signature(),
SignatureVerificationResult::Valid(ed25519_kp.public().0)
);
statement.sign_ecdsa_private(&secp256k1_kp);
assert_eq!(
statement.verify_signature(),
SignatureVerificationResult::Valid(sp_core::hashing::blake2_256(
&secp256k1_kp.public().0
))
);
// set an invalid signature
statement.set_proof(Proof::Sr25519 { signature: [0u8; 64], signer: [0u8; 32] });
assert_eq!(statement.verify_signature(), SignatureVerificationResult::Invalid);
statement.remove_proof();
assert_eq!(statement.verify_signature(), SignatureVerificationResult::NoSignature);
}
}
@@ -0,0 +1,187 @@
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Runtime support for the statement store.
use crate::{Hash, Statement, Topic};
use codec::{Decode, Encode};
use scale_info::TypeInfo;
use sp_runtime::RuntimeDebug;
use sp_runtime_interface::{pass_by::PassByEnum, runtime_interface};
use sp_std::vec::Vec;
#[cfg(feature = "std")]
use sp_externalities::ExternalitiesExt;
/// Information concerning a valid statement.
#[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo)]
pub struct ValidStatement {
/// Max statement count for this account, as calculated by the runtime.
pub max_count: u32,
/// Max total data size for this account, as calculated by the runtime.
pub max_size: u32,
}
/// An reason for an invalid statement.
#[derive(Clone, PartialEq, Eq, Encode, Decode, Copy, RuntimeDebug, TypeInfo)]
pub enum InvalidStatement {
/// Failed proof validation.
BadProof,
/// Missing proof.
NoProof,
/// Validity could not be checked because of internal error.
InternalError,
}
/// The source of the statement.
///
/// Depending on the source we might apply different validation schemes.
#[derive(Copy, Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo)]
pub enum StatementSource {
/// Statement is coming from the on-chain worker.
Chain,
/// Statement has been received from the gossip network.
Network,
/// Statement has been submitted over the local api.
Local,
}
impl StatementSource {
/// Check if the source allows the statement to be resubmitted to the store, extending its
/// expiration date.
pub fn can_be_resubmitted(&self) -> bool {
match self {
StatementSource::Chain | StatementSource::Local => true,
StatementSource::Network => false,
}
}
}
sp_api::decl_runtime_apis! {
/// Runtime API trait for statement validation.
pub trait ValidateStatement {
/// Validate the statement.
fn validate_statement(
source: StatementSource,
statement: Statement,
) -> Result<ValidStatement, InvalidStatement>;
}
}
#[cfg(feature = "std")]
sp_externalities::decl_extension! {
/// The offchain database extension that will be registered at the Substrate externalities.
pub struct StatementStoreExt(std::sync::Arc<dyn crate::StatementStore>);
}
// Host extensions for the runtime.
#[cfg(feature = "std")]
impl StatementStoreExt {
/// Create new instance of externalities extensions.
pub fn new(store: std::sync::Arc<dyn crate::StatementStore>) -> Self {
Self(store)
}
}
/// Submission result.
#[derive(Debug, Eq, PartialEq, Clone, Copy, Encode, Decode, PassByEnum)]
pub enum SubmitResult {
/// Accepted as new.
OkNew,
/// Known statement
OkKnown,
/// Statement failed validation.
Bad,
/// The store is not available.
NotAvailable,
/// Statement could not be inserted because of priority or size checks.
Full,
}
/// Export functions for the WASM host.
#[cfg(feature = "std")]
pub type HostFunctions = (statement_store::HostFunctions,);
/// Host interface
#[runtime_interface]
pub trait StatementStore {
/// Submit a new new statement. The statement will be broadcast to the network.
/// This is meant to be used by the offchain worker.
fn submit_statement(&mut self, statement: Statement) -> SubmitResult {
if let Some(StatementStoreExt(store)) = self.extension::<StatementStoreExt>() {
match store.submit(statement, StatementSource::Chain) {
crate::SubmitResult::New(_) => SubmitResult::OkNew,
crate::SubmitResult::Known => SubmitResult::OkKnown,
crate::SubmitResult::Ignored => SubmitResult::Full,
// This should not happen for `StatementSource::Chain`. An existing statement will
// be overwritten.
crate::SubmitResult::KnownExpired => SubmitResult::Bad,
crate::SubmitResult::Bad(_) => SubmitResult::Bad,
crate::SubmitResult::InternalError(_) => SubmitResult::Bad,
}
} else {
SubmitResult::NotAvailable
}
}
/// Return all statements.
fn statements(&mut self) -> Vec<(Hash, Statement)> {
if let Some(StatementStoreExt(store)) = self.extension::<StatementStoreExt>() {
store.statements().unwrap_or_default()
} else {
Vec::default()
}
}
/// Return the data of all known statements which include all topics and have no `DecryptionKey`
/// field.
fn broadcasts(&mut self, match_all_topics: &[Topic]) -> Vec<Vec<u8>> {
if let Some(StatementStoreExt(store)) = self.extension::<StatementStoreExt>() {
store.broadcasts(match_all_topics).unwrap_or_default()
} else {
Vec::default()
}
}
/// Return the data of all known statements whose decryption key is identified as `dest` (this
/// will generally be the public key or a hash thereof for symmetric ciphers, or a hash of the
/// private key for symmetric ciphers).
fn posted(&mut self, match_all_topics: &[Topic], dest: [u8; 32]) -> Vec<Vec<u8>> {
if let Some(StatementStoreExt(store)) = self.extension::<StatementStoreExt>() {
store.posted(match_all_topics, dest).unwrap_or_default()
} else {
Vec::default()
}
}
/// Return the decrypted data of all known statements whose decryption key is identified as
/// `dest`. The key must be available to the client.
fn posted_clear(&mut self, match_all_topics: &[Topic], dest: [u8; 32]) -> Vec<Vec<u8>> {
if let Some(StatementStoreExt(store)) = self.extension::<StatementStoreExt>() {
store.posted_clear(match_all_topics, dest).unwrap_or_default()
} else {
Vec::default()
}
}
/// Remove a statement from the store by hash.
fn remove(&mut self, hash: &Hash) {
if let Some(StatementStoreExt(store)) = self.extension::<StatementStoreExt>() {
store.remove(hash).unwrap_or_default()
}
}
}
@@ -0,0 +1,90 @@
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub use crate::runtime_api::StatementSource;
use crate::{Hash, Statement, Topic};
/// Statement store error.
#[derive(Debug, Eq, PartialEq, thiserror::Error)]
pub enum Error {
/// Database error.
#[error("Database error: {0:?}")]
Db(String),
/// Error decoding statement structure.
#[error("Error decoding statement: {0:?}")]
Decode(String),
/// Error making runtime call.
#[error("Error calling into the runtime")]
Runtime,
}
#[derive(Debug, PartialEq, Eq)]
/// Network propagation priority.
pub enum NetworkPriority {
/// High priority. Statement should be broadcast to all peers.
High,
/// Low priority.
Low,
}
/// Statement submission outcome
#[derive(Debug, Eq, PartialEq)]
pub enum SubmitResult {
/// Accepted as new with given score
New(NetworkPriority),
/// Known statement
Known,
/// Known statement that's already expired.
KnownExpired,
/// Priority is too low or the size is too big.
Ignored,
/// Statement failed validation.
Bad(&'static str),
/// Internal store error.
InternalError(Error),
}
/// Result type for `Error`
pub type Result<T> = std::result::Result<T, Error>;
/// Statement store API.
pub trait StatementStore: Send + Sync {
/// Return all statements.
fn statements(&self) -> Result<Vec<(Hash, Statement)>>;
/// Get statement by hash.
fn statement(&self, hash: &Hash) -> Result<Option<Statement>>;
/// Return the data of all known statements which include all topics and have no `DecryptionKey`
/// field.
fn broadcasts(&self, match_all_topics: &[Topic]) -> Result<Vec<Vec<u8>>>;
/// Return the data of all known statements whose decryption key is identified as `dest` (this
/// will generally be the public key or a hash thereof for symmetric ciphers, or a hash of the
/// private key for symmetric ciphers).
fn posted(&self, match_all_topics: &[Topic], dest: [u8; 32]) -> Result<Vec<Vec<u8>>>;
/// Return the decrypted data of all known statements whose decryption key is identified as
/// `dest`. The key must be available to the client.
fn posted_clear(&self, match_all_topics: &[Topic], dest: [u8; 32]) -> Result<Vec<Vec<u8>>>;
/// Submit a statement.
fn submit(&self, statement: Statement, source: StatementSource) -> SubmitResult;
/// Remove a statement from the store.
fn remove(&self, hash: &Hash) -> Result<()>;
}
+2
View File
@@ -75,6 +75,7 @@ exceptions = [
{ allow = ["GPL-3.0 WITH Classpath-exception-2.0"], name = "sc-network-sync" },
{ allow = ["GPL-3.0 WITH Classpath-exception-2.0"], name = "sc-network-test" },
{ allow = ["GPL-3.0 WITH Classpath-exception-2.0"], name = "sc-network-transactions" },
{ allow = ["GPL-3.0 WITH Classpath-exception-2.0"], name = "sc-network-statement" },
{ allow = ["GPL-3.0 WITH Classpath-exception-2.0"], name = "sc-offchain" },
{ allow = ["GPL-3.0 WITH Classpath-exception-2.0"], name = "sc-peerset" },
{ allow = ["GPL-3.0 WITH Classpath-exception-2.0"], name = "sc-proposer-metrics" },
@@ -86,6 +87,7 @@ exceptions = [
{ allow = ["GPL-3.0 WITH Classpath-exception-2.0"], name = "sc-service" },
{ allow = ["GPL-3.0 WITH Classpath-exception-2.0"], name = "sc-service-test" },
{ allow = ["GPL-3.0 WITH Classpath-exception-2.0"], name = "sc-state-db" },
{ allow = ["GPL-3.0 WITH Classpath-exception-2.0"], name = "sc-statement-store" },
{ allow = ["GPL-3.0 WITH Classpath-exception-2.0"], name = "sc-storage-monitor" },
{ allow = ["GPL-3.0 WITH Classpath-exception-2.0"], name = "sc-sysinfo" },
{ allow = ["GPL-3.0 WITH Classpath-exception-2.0"], name = "sc-telemetry" },