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
@@ -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<()>;
}