diff --git a/Cargo.lock b/Cargo.lock index a96675bf..2ee4c2b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7626,6 +7626,7 @@ dependencies = [ "jsonrpsee 0.22.5", "pallet-chronicle", "pallet-im-online", + "pallet-opa", "pallet-session", "runtime-chronicle", "sc-basic-authorship", diff --git a/crates/chronicle-arrow/src/query/activity.rs b/crates/chronicle-arrow/src/query/activity.rs index 740d0e65..69f0f3aa 100644 --- a/crates/chronicle-arrow/src/query/activity.rs +++ b/crates/chronicle-arrow/src/query/activity.rs @@ -299,7 +299,7 @@ pub async fn load_activities_by_type( .on(activity::id.eq(activity_block_info::activity_id)), ) .filter(activity::domaintype.eq(typ_value.external_id_part())) - .order(activity_block_info::block_time) + .order(activity_block_info::block_time.desc()) .select((Activity::as_select(), Namespace::as_select())) .offset(position as i64) .limit(max_records as i64) @@ -313,7 +313,7 @@ pub async fn load_activities_by_type( .on(activity::id.eq(activity_block_info::activity_id)), ) .filter(activity::domaintype.is_null()) - .order(activity_block_info::block_time) + .order(activity_block_info::block_time.desc()) .select((Activity::as_select(), Namespace::as_select())) .offset(position as i64) .limit(max_records as i64) diff --git a/crates/chronicle-persistence/src/queryable.rs b/crates/chronicle-persistence/src/queryable.rs index 25b0d20f..b31a8991 100644 --- a/crates/chronicle-persistence/src/queryable.rs +++ b/crates/chronicle-persistence/src/queryable.rs @@ -30,14 +30,6 @@ pub struct Entity { pub domaintype: Option, } -#[derive(Queryable, Selectable, SimpleObject)] -#[diesel(table_name = crate::schema::entity_block_info)] -pub struct EntityBlockInfo { - pub entity_id: i32, - pub block_time: Option, - pub block_hash: Option, -} - #[derive(Default, Queryable)] pub struct Namespace { _id: i32, diff --git a/crates/chronicle/src/bootstrap/cli.rs b/crates/chronicle/src/bootstrap/cli.rs index f0c09511..5a476e56 100644 --- a/crates/chronicle/src/bootstrap/cli.rs +++ b/crates/chronicle/src/bootstrap/cli.rs @@ -174,7 +174,6 @@ impl CliError { } } -/// Ugly but we need this until ! is stable, see impl From for CliError { fn from(_: Infallible) -> Self { unreachable!() @@ -372,8 +371,6 @@ fn attributes_from( impl SubCommand for AgentCliModel { fn as_cmd(&self) -> Command { - let cmd = Command::new(&*self.external_id).about(&*self.about); - let mut define = Command::new("define") .about(&*self.define_about) .arg(Arg::new("external_id") diff --git a/crates/common/src/opa/core.rs b/crates/common/src/opa/core.rs index 0d8318f3..19ddbf92 100644 --- a/crates/common/src/opa/core.rs +++ b/crates/common/src/opa/core.rs @@ -136,13 +136,6 @@ impl AsRef<[u8]> for KeyAddress { } } -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -// This message is used to bootstrap the root key for a newly created authz tp, -// it can only be executed once -pub struct BootstrapRoot { - pub public_key: PemEncoded, -} - #[cfg_attr( feature = "parity-encoding", derive( @@ -153,8 +146,7 @@ pub struct BootstrapRoot { scale_decode::DecodeAsType, ) )] -#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize,))] -#[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq, Hash)] pub struct PemEncoded(String); impl PemEncoded { @@ -242,7 +234,6 @@ pub struct OpaSubmission { #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum Payload { - BootstrapRoot(BootstrapRoot), SignedOperation(SignedOperation), } @@ -522,7 +513,6 @@ pub mod codec { #[derive(Encode, EncodeAsType, DecodeAsType, Decode, Debug, TypeInfo, Clone, PartialEq, Eq)] pub enum PayloadV1 { - BootstrapRoot(BootstrapRootV1), SignedOperation(SignedOperationV1), } @@ -582,23 +572,9 @@ pub mod codec { } } - impl From for BootstrapRoot { - fn from(item: codec::BootstrapRootV1) -> Self { - Self { public_key: item.public_key } - } - } - - impl From for codec::BootstrapRootV1 { - fn from(item: BootstrapRoot) -> Self { - tracing::debug!(target: "codec_conversion", "Converting BootstrapRoot to BootstrapRootV1"); - Self { public_key: item.public_key } - } - } - impl From for Payload { fn from(item: codec::PayloadV1) -> Self { match item { - codec::PayloadV1::BootstrapRoot(v) => Self::BootstrapRoot(v.into()), codec::PayloadV1::SignedOperation(v) => Self::SignedOperation(v.into()), } } @@ -623,10 +599,6 @@ pub mod codec { correlation_id: item.correlation_id, span_id: item.span_id, payload: match item.payload { - Payload::BootstrapRoot(v) => { - tracing::trace!(target: "codec_conversion", "Payload is BootstrapRoot"); - codec::PayloadV1::BootstrapRoot(v.into()) - }, Payload::SignedOperation(v) => { tracing::trace!(target: "codec_conversion", "Payload is SignedOperation"); codec::PayloadV1::SignedOperation(v.into()) diff --git a/crates/common/src/prov/model/proptest.rs b/crates/common/src/prov/model/proptest.rs index 5e98fc62..9d564e9e 100644 --- a/crates/common/src/prov/model/proptest.rs +++ b/crates/common/src/prov/model/proptest.rs @@ -605,5 +605,8 @@ proptest! { let lhs_json_2 = compact_json(&prov).clone(); prop_assert_eq!( lhs_json.clone().to_string(), lhs_json_2.to_string()); } + + + } } diff --git a/crates/embedded-substrate/src/lib.rs b/crates/embedded-substrate/src/lib.rs index 377f802c..ab28015a 100644 --- a/crates/embedded-substrate/src/lib.rs +++ b/crates/embedded-substrate/src/lib.rs @@ -3,7 +3,6 @@ use protocol_substrate::SubxtClientError; use protocol_substrate_chronicle::ChronicleSubstrateClient; use sc_cli::{print_node_infos, CliConfiguration, Signals, SubstrateCli}; use subxt::{ - config::ExtrinsicParams, ext::futures::{pin_mut, FutureExt}, }; use tempfile::TempDir; diff --git a/crates/opactl/src/cli.rs b/crates/opactl/src/cli.rs index 7f2ef32d..71451515 100644 --- a/crates/opactl/src/cli.rs +++ b/crates/opactl/src/cli.rs @@ -32,14 +32,6 @@ fn wait_args(command: Command) -> Command { ) } -fn bootstrap() -> Command { - wait_args( - Command::new("bootstrap") - .about("Initialize the OPA transaction processor with a root key from the keystore") - .arg(batcher_key()), - ) -} - fn generate() -> Command { Command::new("generate") .arg(Arg::new("output").short('o').long("output").num_args(0..=1).help( @@ -306,7 +298,6 @@ pub fn cli() -> Command { .env("SAWTOOTH_ADDRESS") .default_value("tcp://localhost:4004"), ) - .subcommand(bootstrap()) .subcommand(generate()) .subcommand(rotate_root()) .subcommand(register_key()) diff --git a/crates/opactl/src/main.rs b/crates/opactl/src/main.rs index 74de3f8b..cb4d1aed 100644 --- a/crates/opactl/src/main.rs +++ b/crates/opactl/src/main.rs @@ -251,17 +251,6 @@ async fn dispatch_args< let _entered = span.enter(); let span_id = span.id().map(|x| x.into_u64()).unwrap_or(u64::MAX); match matches.subcommand() { - Some(("bootstrap", command_matches)) => { - let signing = configure_signing(vec![], &matches, command_matches).await?; - let bootstrap = SubmissionBuilder::bootstrap_root(signing.opa_verifying().await?) - .build(span_id, Uuid::new_v4()); - Ok(handle_wait( - command_matches, - client, - OpaTransaction::bootstrap_root(bootstrap, &signing).await?, - ) - .await?) - }, Some(("generate", matches)) => { let key = SecretKey::random(StdRng::from_entropy()); let key = key.to_pkcs8_pem(LineEnding::CRLF).map_err(|_| OpaCtlError::Pkcs8)?; diff --git a/crates/opactl/src/test/mockchain.rs b/crates/opactl/src/test/mockchain.rs index 645bac63..49e30529 100644 --- a/crates/opactl/src/test/mockchain.rs +++ b/crates/opactl/src/test/mockchain.rs @@ -51,9 +51,9 @@ impl frame_system::Config for Test { impl pallet_timestamp::Config for Test { type MinimumPeriod = ConstU64<1>; + type Moment = u64; type OnTimestampSet = (); type WeightInfo = (); - type Moment = u64; } impl pallet_opa::Config for Test { @@ -62,7 +62,40 @@ impl pallet_opa::Config for Test { type WeightInfo = (); } +use k256::SecretKey; +use rand::rngs::StdRng; +use rand_core::SeedableRng; + +pub struct GenesisConfig { + pub root_key: SecretKey, +} + +impl Default for GenesisConfig { + fn default() -> Self { + let root_key = SecretKey::random(StdRng::from_seed([0u8; 32])); + GenesisConfig { root_key } + } +} + +impl GenesisConfig { + pub fn build_storage(&self) -> sp_io::TestExternalities { + use k256::pkcs8::EncodePublicKey; + let mut storage = frame_system::GenesisConfig::::default().build_storage().unwrap(); + // Insert the root key into the storage + storage.top.insert( + b"pallet_opa::root_key".to_vec(), + self.root_key + .public_key() + .to_public_key_pem(k256::pkcs8::LineEnding::CRLF) + .unwrap() + .as_bytes() + .to_vec(), + ); + storage.into() + } +} + // Build genesis storage according to the mock runtime. pub fn new_test_ext() -> sp_io::TestExternalities { - frame_system::GenesisConfig::::default().build_storage().unwrap().into() + GenesisConfig::default().build_storage() } diff --git a/crates/pallet-opa/src/lib.rs b/crates/pallet-opa/src/lib.rs index cbdb18f7..2714c91a 100644 --- a/crates/pallet-opa/src/lib.rs +++ b/crates/pallet-opa/src/lib.rs @@ -8,9 +8,9 @@ use common::{ k256::ecdsa::{Signature, VerifyingKey}, opa::{ codec::{NewPublicKeyV1, OpaSubmissionV1, PayloadV1, SignedOperationV1}, - BootstrapRoot, KeyAddress, KeyRegistration, Keys, OpaSubmission, Operation, Payload, - PolicyAddress, PolicyMeta, PolicyMetaAddress, RegisterKey, RotateKey, SetPolicy, - SignedOperation, SignedOperationPayload, + KeyAddress, KeyRegistration, Keys, OpaSubmission, Operation, Payload, PolicyAddress, + PolicyMeta, PolicyMetaAddress, RegisterKey, RotateKey, SetPolicy, SignedOperation, + SignedOperationPayload, }, }; @@ -71,7 +71,6 @@ fn verify_signed_operation( ) -> Result<(), OpaError> { use k256::ecdsa::signature::Verifier; match &submission.payload { - PayloadV1::BootstrapRoot(_) => Ok(()), PayloadV1::SignedOperation(SignedOperationV1 { payload, verifying_key, signature }) => { if root_keys.is_none() { error!("No registered root keys for signature verification"); @@ -111,31 +110,7 @@ fn apply_signed_operation( correlation_id: ChronicleTransactionId, payload: Payload, ) -> Result<(), OpaError> { - use scale_info::prelude::string::ToString; match payload { - Payload::BootstrapRoot(BootstrapRoot { public_key }) => { - let existing_key = pallet::KeyStore::::try_get(key_address("root")); - - if existing_key.is_ok() { - error!("OPA TP has already been bootstrapped"); - return Err(OpaError::InvalidOperation); - } - - let keys = Keys { - id: "root".to_string(), - current: KeyRegistration { key: public_key, version: 0 }, - expired: None, - }; - - pallet::KeyStore::::set(key_address("root"), Some(keys.clone().into())); - - pallet::Pallet::::deposit_event(pallet::Event::::KeyUpdate( - keys.into(), - correlation_id, - )); - - Ok(()) - }, Payload::SignedOperation(SignedOperation { payload: SignedOperationPayload { operation }, verifying_key: _, @@ -291,9 +266,23 @@ fn root_keys_from_state() -> Result, OpaError> { #[frame_support::pallet] pub mod pallet { use super::*; + use common::opa::PemEncoded; use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; + /// Genesis configuration, whether or not we need to enforce OPA policies + #[pallet::genesis_config] + pub struct GenesisConfig { + pub root_account: Option, + pub _phantom: PhantomData, + } + + impl Default for GenesisConfig { + fn default() -> Self { + Self { root_account: None, _phantom: PhantomData } + } + } + #[pallet::pallet] pub struct Pallet(_); @@ -355,13 +344,31 @@ pub mod pallet { } } + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + use scale_info::prelude::string::ToString; + if let Some(key) = &self.root_account { + pallet::KeyStore::::set( + key_address("root"), + Some( + Keys { + id: "root".to_string(), + current: KeyRegistration { key: key.clone(), version: 0 }, + expired: None, + } + .into(), + ), + ); + } + } + } + // Dispatchable functions allows users to interact with the pallet and invoke state changes. // These functions materialize as "extrinsics", which are often compared to transactions. // Dispatchable functions must be annotated with a weight and must return a DispatchResult. #[pallet::call] impl Pallet { - // Apply a vector of chronicle operations, yielding an event that indicates state change or - // contradiction #[pallet::call_index(0)] #[pallet::weight(T::WeightInfo::apply())] pub fn apply(origin: OriginFor, submission: T::OpaSubmission) -> DispatchResult { diff --git a/crates/protocol-substrate-opa/src/submission_builder.rs b/crates/protocol-substrate-opa/src/submission_builder.rs index c0010516..364ec5fc 100644 --- a/crates/protocol-substrate-opa/src/submission_builder.rs +++ b/crates/protocol-substrate-opa/src/submission_builder.rs @@ -16,18 +16,13 @@ use common::{ }, opa::{ codec::{NewPublicKeyV1, SignedOperationPayloadV1}, - BootstrapRoot, NewPublicKey, OpaSubmission, Operation, Payload, Policy, RegisterKey, - RotateKey, SetPolicy, SignedOperation, SignedOperationPayload, + NewPublicKey, OpaSubmission, Operation, Payload, Policy, RegisterKey, RotateKey, SetPolicy, + SignedOperation, SignedOperationPayload, }, }; use subxt::ext::codec::Encode; use uuid::Uuid; -fn bootstrap_root(public_key: VerifyingKey) -> BootstrapRoot { - let public_key: PublicKey = public_key.into(); - BootstrapRoot { public_key: public_key.to_public_key_pem(LineEnding::CRLF).unwrap().into() } -} - fn register_key( id: impl AsRef, public_key: &VerifyingKey, @@ -76,7 +71,6 @@ fn set_policy(id: impl AsRef, policy: Vec) -> SetPolicy { } enum BuildingMessage { - BootstrapRoot(BootstrapRoot), RegisterKey(SignedOperation), RotateKey(SignedOperation), SetPolicy(SignedOperation), @@ -87,10 +81,6 @@ pub struct SubmissionBuilder { } impl SubmissionBuilder { - pub fn bootstrap_root(public_key: VerifyingKey) -> Self { - Self { message: Some(BuildingMessage::BootstrapRoot(bootstrap_root(public_key))) } - } - pub async fn register_key( id: impl AsRef, new_key: &str, @@ -190,7 +180,6 @@ impl SubmissionBuilder { }, version: "1.0".to_string(), payload: match self.message { - Some(BuildingMessage::BootstrapRoot(message)) => Payload::BootstrapRoot(message), Some(BuildingMessage::RotateKey(message)) => Payload::SignedOperation(message), Some(BuildingMessage::SetPolicy(message)) => Payload::SignedOperation(message), Some(BuildingMessage::RegisterKey(message)) => Payload::SignedOperation(message), diff --git a/node/node-chronicle/Cargo.toml b/node/node-chronicle/Cargo.toml index 19fec50e..8f264865 100644 --- a/node/node-chronicle/Cargo.toml +++ b/node/node-chronicle/Cargo.toml @@ -77,6 +77,7 @@ try-runtime-cli = { git = 'https://github.com/paritytech/polkadot-sdk', tag = 'p # Local Dependencies runtime-chronicle = { path = "../runtime-chronicle" } pallet-chronicle = { path = "../../crates/pallet-chronicle"} +pallet-opa = { path = "../../crates/pallet-opa"} common = {path = "../../crates/common"} [build-dependencies] diff --git a/node/node-chronicle/src/chain_spec.rs b/node/node-chronicle/src/chain_spec.rs index 66a85cd6..78de3420 100644 --- a/node/node-chronicle/src/chain_spec.rs +++ b/node/node-chronicle/src/chain_spec.rs @@ -1,5 +1,9 @@ use std::path::Path; +use common::{ + k256::{pkcs8::LineEnding, SecretKey}, + opa::PemEncoded, +}; use runtime_chronicle::{ opaque::SessionKeys, pallet_chronicle, AccountId, AuraConfig, GrandpaConfig, Runtime, RuntimeGenesisConfig, SessionConfig, Signature, SudoConfig, SystemConfig, ValidatorSetConfig, @@ -67,6 +71,15 @@ pub fn authority_keys_from_keystore(p: &std::path::Path) -> (AccountId, AuraId, (AccountPublic::from(authority_key).into_account(), authority_key.into(), grandpa_key.into()) } +pub fn opa_key_from_keystore(p: &std::path::Path) -> Option { + use common::k256::PublicKey; + let keystore = LocalKeystore::open(p, None).ok()?; + + let opa_key = Keystore::ecdsa_public_keys(&keystore, KeyTypeId(*b"opak")).into_iter().next()?; + + PublicKey::from_sec1_bytes(opa_key.as_ref()).ok() +} + pub fn development_config() -> Result { let wasm_binary = WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?; log::info!("development configuration"); @@ -79,6 +92,7 @@ pub fn development_config() -> Result { serde_json::to_value(genesis( vec![authority_keys_from_seed("Alice")], get_account_id_from_seed::("Alice"), + None, true, )) .expect("Genesis config should be serializable"), @@ -99,6 +113,7 @@ pub fn local_testnet_config() -> Result { to_value(genesis( vec![authority_keys_from_seed("Alice")], get_account_id_from_seed::("Alice"), + None, true, )) .expect("Genesis config should be serializable"), @@ -115,8 +130,10 @@ fn session_keys(aura: AuraId, grandpa: GrandpaId) -> SessionKeys { fn genesis( initial_authorities: Vec<(AccountId, AuraId, GrandpaId)>, root_key: AccountId, + opa_root_key: Option, _enable_println: bool, ) -> RuntimeGenesisConfig { + use common::k256::pkcs8::EncodePublicKey; RuntimeGenesisConfig { system: SystemConfig { ..Default::default() }, sudo: SudoConfig { @@ -135,6 +152,11 @@ fn genesis( }, aura: AuraConfig { authorities: vec![] }, grandpa: GrandpaConfig { ..Default::default() }, + opa: pallet_opa::GenesisConfig:: { + root_account: opa_root_key + .map(|key| PemEncoded::new(key.to_public_key_pem(LineEnding::CRLF).unwrap())), + ..Default::default() + }, } } @@ -142,15 +164,20 @@ pub fn chronicle_config() -> Result { let wasm_binary = WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?; let (root_key, aura_key, grandpa_key) = authority_keys_from_keystore(Path::new("/keystore/")); - + let opa_key = opa_key_from_keystore(Path::new("/keystore/")); log::info!("Private network configuration"); Ok(ChainSpec::builder(wasm_binary, None) .with_name("Chronicle") .with_id("chronicle") .with_chain_type(ChainType::Live) .with_genesis_config( - to_value(genesis(vec![(root_key.clone(), aura_key, grandpa_key)], root_key, true)) - .expect("Genesis config should be serializable"), + to_value(genesis( + vec![(root_key.clone(), aura_key, grandpa_key)], + root_key, + opa_key, + true, + )) + .expect("Genesis config should be serializable"), ) .with_protocol_id("chronicle") .build()) diff --git a/terraform/chronicle-substrate/templates/bootnode.tmpl b/terraform/chronicle-substrate/templates/bootnode.tmpl index 123e1d9e..191a1da7 100644 --- a/terraform/chronicle-substrate/templates/bootnode.tmpl +++ b/terraform/chronicle-substrate/templates/bootnode.tmpl @@ -63,6 +63,11 @@ node: scheme: ed25519 vaultPath: kv/chronicle-substrate/grankey vaultKey: secretSeed + - name: opak + type: opak + schema: ecsda + vaultPath: kv/chronicle-substrate/oparoot + vaultKey: secretSeed nodeKey: name: bootnode_key vaultKey: key diff --git a/terraform/vault-configuration/Makefile b/terraform/vault-configuration/Makefile index d80185f7..8773a836 100644 --- a/terraform/vault-configuration/Makefile +++ b/terraform/vault-configuration/Makefile @@ -10,6 +10,9 @@ export TF_VAR_grankey_key TF_VAR_root_key := $(shell docker run $(KEYGEN_IMAGE):$(KEYGEN_TAG) node-chronicle key generate --network substrate --output-type json) export TF_VAR_root_key +TF_VAR_opa_root_key := $(shell docker run $(KEYGEN_IMAGE):$(KEYGEN_TAG) node-chronicle key generate --network substrate --scheme ecdsa --output-type json) +export TF_VAR_opa_root_key + TF_VAR_online_key := $(shell docker run $(KEYGEN_IMAGE):$(KEYGEN_TAG) node-chronicle key generate --network substrate --output-type json) export TF_VAR_online_key diff --git a/terraform/vault-configuration/variables.tf b/terraform/vault-configuration/variables.tf index d5dc10a9..5a7979a6 100644 --- a/terraform/vault-configuration/variables.tf +++ b/terraform/vault-configuration/variables.tf @@ -35,6 +35,12 @@ variable "root_key" { sensitive = true } +variable "opa_root_key" { + description = "The opa root account key for the Chronicle Substrate node." + type = string + sensitive = true +} + variable "online_key" { description = "The i'm online account key for the Chronicle Substrate node." type = string diff --git a/terraform/vault-configuration/vault.tf b/terraform/vault-configuration/vault.tf index e127baaf..7e83ef89 100644 --- a/terraform/vault-configuration/vault.tf +++ b/terraform/vault-configuration/vault.tf @@ -99,10 +99,11 @@ resource "vault_kv_secret_v2" "rootkey" { } } -resource "vault_kv_secret_v2" "onlinekey" { - name = "online" + +resource "vault_kv_secret_v2" "opa_root_key" { + name = "oparoot" mount = vault_mount.chronicle_substrate.path - data_json = var.online_key + data_json = var.opa_root_key #Once secrets are set, do not update lifecycle {