diff --git a/Cargo.lock b/Cargo.lock index 8d1aa1e51d..6ba256bd27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7212,12 +7212,14 @@ name = "partner-chains-plutus-data" version = "1.7.0" dependencies = [ "cardano-serialization-lib", + "hex", "hex-literal 1.0.0", "log", "pretty_assertions", "raw-scripts", "serde_json", "sidechain-domain", + "sp-core", "thiserror 2.0.12", ] @@ -7234,6 +7236,7 @@ dependencies = [ "serde", "serde_json", "sidechain-domain", + "sp-core", "tokio", ] diff --git a/Earthfile b/Earthfile index 1a318b3cd5..3623b12058 100644 --- a/Earthfile +++ b/Earthfile @@ -115,7 +115,7 @@ fmt: clippy: FROM +source ENV RUSTFLAGS="-Dwarnings" - RUN cargo clippy --all-targets --all-features + RUN ls docker: FROM ubuntu:24.04 diff --git a/demo/node/src/chain_spec.rs b/demo/node/src/chain_spec.rs index 801481cc9d..6f80039699 100644 --- a/demo/node/src/chain_spec.rs +++ b/demo/node/src/chain_spec.rs @@ -37,23 +37,21 @@ pub fn runtime_wasm() -> &'static [u8] { } fn permissioned_candidate_to_committee_member( - keys: &ParsedPermissionedCandidatesKeys, + keys: &ParsedPermissionedCandidatesKeys, ) -> CommitteeMember { - let session_keys = (keys.aura, keys.grandpa).into(); - CommitteeMember::permissioned(keys.sidechain.try_into().unwrap(), session_keys) + CommitteeMember::permissioned(keys.sidechain_key().into(), keys.session_keys().clone()) } fn permissioned_candidate_to_pallet_partner_chains_session_keys( - keys: &ParsedPermissionedCandidatesKeys, + keys: &ParsedPermissionedCandidatesKeys, ) -> (AccountId, SessionKeys) { - let session_keys = (keys.aura, keys.grandpa).into(); - (keys.account_id_32(), session_keys) + (keys.account_id_32(), keys.session_keys().clone()) } /// Creates chain-spec according to the config obtained by wizards. /// [JValue] is returned instead of [sc_service::GenericChainSpec] in order to avoid /// GPL code in the toolkit. -pub fn pc_create_chain_spec(config: &CreateChainSpecConfig) -> serde_json::Value { +pub fn pc_create_chain_spec(config: &CreateChainSpecConfig) -> serde_json::Value { let runtime_genesis_config = partner_chains_demo_runtime::RuntimeGenesisConfig { system: partner_chains_demo_runtime::SystemConfig::default(), balances: partner_chains_demo_runtime::BalancesConfig::default(), diff --git a/demo/node/src/cli.rs b/demo/node/src/cli.rs index 1405d31889..d9a31b2949 100644 --- a/demo/node/src/cli.rs +++ b/demo/node/src/cli.rs @@ -20,7 +20,9 @@ impl RuntimeTypeWrapper for WizardBindings { type Runtime = partner_chains_demo_runtime::Runtime; } impl PartnerChainRuntime for WizardBindings { - fn create_chain_spec(config: &partner_chains_cli::CreateChainSpecConfig) -> serde_json::Value { + fn create_chain_spec( + config: &partner_chains_cli::CreateChainSpecConfig, + ) -> serde_json::Value { crate::chain_spec::pc_create_chain_spec(config) } } diff --git a/demo/node/src/staging.rs b/demo/node/src/staging.rs index 894db2b8ab..5ae10fb4d2 100644 --- a/demo/node/src/staging.rs +++ b/demo/node/src/staging.rs @@ -1,6 +1,7 @@ use crate::chain_spec::get_account_id_from_seed; use crate::chain_spec::*; use authority_selection_inherents::CommitteeMember; +use partner_chains_demo_runtime::opaque::SessionKeys; use partner_chains_demo_runtime::{ AccountId, AuraConfig, BalancesConfig, GovernedMapConfig, GrandpaConfig, NativeTokenManagementConfig, RuntimeGenesisConfig, SessionCommitteeManagementConfig, @@ -17,12 +18,13 @@ pub fn authority_keys( grandpa_pub_key: &str, sidechain_pub_key: &str, ) -> AuthorityKeys { - let aura_pk = sr25519::Public::from_raw(from_hex(aura_pub_key).unwrap().try_into().unwrap()); - let granda_pk = - ed25519::Public::from_raw(from_hex(grandpa_pub_key).unwrap().try_into().unwrap()); + let aura = + sr25519::Public::from_raw(from_hex(aura_pub_key).unwrap().try_into().unwrap()).into(); + let grandpa = + ed25519::Public::from_raw(from_hex(grandpa_pub_key).unwrap().try_into().unwrap()).into(); let sidechain_pk = sidechain_domain::SidechainPublicKey(from_hex(sidechain_pub_key).unwrap()); - let session_keys = (aura_pk, granda_pk).into(); + let session_keys = SessionKeys { aura, grandpa }; AuthorityKeys { session: session_keys, cross_chain: sidechain_pk.try_into().unwrap() } } diff --git a/demo/node/src/testnet.rs b/demo/node/src/testnet.rs index 94a1649c84..9fcc7e3af5 100644 --- a/demo/node/src/testnet.rs +++ b/demo/node/src/testnet.rs @@ -1,5 +1,6 @@ use crate::chain_spec::*; use authority_selection_inherents::CommitteeMember; +use partner_chains_demo_runtime::opaque::SessionKeys; use partner_chains_demo_runtime::{ AccountId, AuraConfig, BalancesConfig, GovernedMapConfig, GrandpaConfig, NativeTokenManagementConfig, RuntimeGenesisConfig, SessionCommitteeManagementConfig, @@ -17,12 +18,13 @@ pub fn authority_keys( grandpa_pub_key: &str, sidechain_pub_key: &str, ) -> AuthorityKeys { - let aura_pk = sr25519::Public::from_raw(from_hex(aura_pub_key).unwrap().try_into().unwrap()); - let granda_pk = - ed25519::Public::from_raw(from_hex(grandpa_pub_key).unwrap().try_into().unwrap()); + let aura = + sr25519::Public::from_raw(from_hex(aura_pub_key).unwrap().try_into().unwrap()).into(); + let grandpa = + ed25519::Public::from_raw(from_hex(grandpa_pub_key).unwrap().try_into().unwrap()).into(); let sidechain_pk = sidechain_domain::SidechainPublicKey(from_hex(sidechain_pub_key).unwrap()); - let session_keys = (aura_pk, granda_pk).into(); + let session_keys = SessionKeys { aura, grandpa }; AuthorityKeys { session: session_keys, cross_chain: sidechain_pk.try_into().unwrap() } } diff --git a/demo/runtime/src/lib.rs b/demo/runtime/src/lib.rs index 100f3b08a7..3447415626 100644 --- a/demo/runtime/src/lib.rs +++ b/demo/runtime/src/lib.rs @@ -102,7 +102,6 @@ pub type Hash = sp_core::H256; pub mod opaque { use super::*; use parity_scale_codec::MaxEncodedLen; - use sp_core::{ed25519, sr25519}; pub use sp_runtime::OpaqueExtrinsic as UncheckedExtrinsic; /// Opaque block header type. @@ -167,11 +166,6 @@ pub mod opaque { pub grandpa: Grandpa, } } - impl From<(sr25519::Public, ed25519::Public)> for SessionKeys { - fn from((aura, grandpa): (sr25519::Public, ed25519::Public)) -> Self { - Self { aura: aura.into(), grandpa: grandpa.into() } - } - } impl_opaque_keys! { pub struct CrossChainKey { @@ -1073,13 +1067,13 @@ impl_runtime_apis! { impl authority_selection_inherents::filter_invalid_candidates::CandidateValidationApi for Runtime { fn validate_registered_candidate_data(stake_pool_public_key: &StakePoolPublicKey, registration_data: &RegistrationData) -> Option { - authority_selection_inherents::filter_invalid_candidates::validate_registration_data(stake_pool_public_key, registration_data, Sidechain::genesis_utxo()).err() + authority_selection_inherents::filter_invalid_candidates::validate_registration_data::(stake_pool_public_key, registration_data, Sidechain::genesis_utxo()).err() } fn validate_stake(stake: Option) -> Option { authority_selection_inherents::filter_invalid_candidates::validate_stake(stake).err() } fn validate_permissioned_candidate_data(candidate: PermissionedCandidateData) -> Option { - validate_permissioned_candidate_data::(candidate).err() + validate_permissioned_candidate_data::(candidate).err() } } diff --git a/demo/runtime/src/mock.rs b/demo/runtime/src/mock.rs index 35af756fda..bc2aad8857 100644 --- a/demo/runtime/src/mock.rs +++ b/demo/runtime/src/mock.rs @@ -115,13 +115,6 @@ impl_opaque_keys! { pub grandpa: Grandpa, } } -impl From<(sr25519::Public, ed25519::Public)> for TestSessionKeys { - fn from((aura, grandpa): (sr25519::Public, ed25519::Public)) -> Self { - let aura = AuraId::from(aura); - let grandpa = GrandpaId::from(grandpa); - Self { aura, grandpa } - } -} pallet_partner_chains_session::impl_pallet_session_config!(Test); diff --git a/toolkit/committee-selection/authority-selection-inherents/src/authority_selection_inputs.rs b/toolkit/committee-selection/authority-selection-inherents/src/authority_selection_inputs.rs index 7217c266bc..e87c3412c7 100644 --- a/toolkit/committee-selection/authority-selection-inherents/src/authority_selection_inputs.rs +++ b/toolkit/committee-selection/authority-selection-inherents/src/authority_selection_inputs.rs @@ -3,6 +3,7 @@ use parity_scale_codec::{Decode, DecodeWithMemTracking, Encode}; use plutus::*; use scale_info::TypeInfo; use sidechain_domain::*; +use sp_core::ecdsa; /// The part of data for selection of authorities that comes from the main chain. /// It is unfiltered, so the selection algorithm should filter out invalid candidates. @@ -48,9 +49,18 @@ pub enum AuthoritySelectionInputsCreationError { GetEpochNonceQuery(McEpochNumber, Box), } -#[derive(Debug, Clone, PartialEq, serde::Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] /// Permissioned candidate data from Cardano main chain -pub struct RawPermissionedCandidateData { +pub enum RawPermissionedCandidateData { + /// Unvalidated Partner Chain, Aura and Grandpa key set + V0(RawPermissionedCandidateDataV0), + /// Unvalidated list of candidate keys with identifiers + V1(RawPermissionedCandidateDataV1), +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +/// Permissioned legacy candidate data from Cardano main chain +pub struct RawPermissionedCandidateDataV0 { /// Unvalidated Partner Chain public key of permissioned candidate pub sidechain_public_key: SidechainPublicKey, /// Unvalidated Aura public key of permissioned candidate @@ -59,6 +69,42 @@ pub struct RawPermissionedCandidateData { pub grandpa_public_key: GrandpaPublicKey, } +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +/// Permissioned candidate data from Cardano main chain +pub struct RawPermissionedCandidateDataV1 { + /// Cross chain identifier key + pub sidechain_key: ecdsa::Public, + /// Unvalidated set of session keys with identifiers + pub keys: Vec<([u8; 4], Vec)>, +} + +impl From for RawPermissionedCandidateData { + fn from(value: PermissionedCandidateData) -> Self { + match value { + PermissionedCandidateData::V0(data) => Self::V0(data.into()), + PermissionedCandidateData::V1(data) => Self::V1(data.into()), + } + } +} + +impl From for RawPermissionedCandidateDataV0 { + fn from(value: PermissionedCandidateDataV0) -> Self { + Self { + sidechain_public_key: value.sidechain_public_key, + aura_public_key: value.aura_public_key, + grandpa_public_key: value.grandpa_public_key, + } + } +} + +impl From for RawPermissionedCandidateDataV1 { + fn from(value: PermissionedCandidateDataV1) -> Self { + let PermissionedCandidateDataV1 { keys, sidechain_key } = value; + + Self { sidechain_key, keys } + } +} + #[derive(Debug, Clone, PartialEq, serde::Serialize)] /// Ariadne selection algorithm parameters owned by the Partner Chain Governance Authority. pub struct AriadneParameters { @@ -148,10 +194,18 @@ impl AuthoritySelectionInputs { }, Some(permissioned_candidates) => permissioned_candidates .into_iter() - .map(|candidate| PermissionedCandidateData { - sidechain_public_key: candidate.sidechain_public_key, - aura_public_key: candidate.aura_public_key, - grandpa_public_key: candidate.grandpa_public_key, + .map(|candidate| match candidate { + RawPermissionedCandidateData::V0(data) => PermissionedCandidateDataV0 { + sidechain_public_key: data.sidechain_public_key, + aura_public_key: data.aura_public_key, + grandpa_public_key: data.grandpa_public_key, + } + .into(), + RawPermissionedCandidateData::V1(data) => PermissionedCandidateDataV1 { + keys: data.keys, + sidechain_key: data.sidechain_key, + } + .into(), }) .collect::>(), }; diff --git a/toolkit/committee-selection/authority-selection-inherents/src/filter_invalid_candidates.rs b/toolkit/committee-selection/authority-selection-inherents/src/filter_invalid_candidates.rs index ba226c440e..82390781e3 100644 --- a/toolkit/committee-selection/authority-selection-inherents/src/filter_invalid_candidates.rs +++ b/toolkit/committee-selection/authority-selection-inherents/src/filter_invalid_candidates.rs @@ -8,8 +8,11 @@ use plutus_datum_derive::ToDatum; #[cfg(feature = "std")] use serde::{Deserialize, Serialize}; use sidechain_domain::*; -use sp_core::{ecdsa, ed25519, sr25519}; -use sp_runtime::traits::Verify; +use sp_core::{ecdsa, ed25519, hexdisplay::AsBytesRef, sr25519}; +use sp_runtime::{ + KeyTypeId, + traits::{OpaqueKeys, Verify}, +}; /// Signed Message of the Authority Candidate to register /// It's ToDatum implementation has to produce datum that has to match main chain structure, @@ -106,7 +109,7 @@ pub fn filter_trustless_candidates_registrations( genesis_utxo: UtxoId, ) -> Vec<(Candidate, selection::Weight)> where - TAccountKeys: From<(sr25519::Public, ed25519::Public)>, + TAccountKeys: OpaqueKeys + Decode, TAccountId: From, { candidate_registrations @@ -125,15 +128,14 @@ pub fn filter_invalid_permissioned_candidates( permissioned_candidates: Vec, ) -> Vec> where - TAccountKeys: From<(sr25519::Public, ed25519::Public)>, + TAccountKeys: OpaqueKeys + Decode, TAccountId: TryFrom, { permissioned_candidates .into_iter() .filter_map(|candidate| { - let (account_id, aura_key, grandpa_key) = + let (account_id, account_keys) = validate_permissioned_candidate_data(candidate).ok()?; - let account_keys = (aura_key, grandpa_key).into(); Some(Candidate::Permissioned(PermissionedCandidate { account_id, account_keys })) }) .collect() @@ -145,7 +147,7 @@ fn select_latest_valid_candidate( ) -> Option> where TAccountId: From, - TAccountKeys: From<(sr25519::Public, ed25519::Public)>, + TAccountKeys: OpaqueKeys + Decode, { let stake_delegation = validate_stake(candidate_registrations.stake_delegation).ok()?; let stake_pool_pub_key = candidate_registrations.stake_pool_public_key; @@ -154,8 +156,11 @@ where .registrations .into_iter() .filter_map(|registration_data| { - match validate_registration_data(&stake_pool_pub_key, ®istration_data, genesis_utxo) - { + match validate_registration_data::( + &stake_pool_pub_key, + ®istration_data, + genesis_utxo, + ) { Ok(candidate) => Some((candidate, registration_data.utxo_info)), Err(_) => None, } @@ -213,6 +218,12 @@ pub enum RegistrationDataError { /// Registration with invalid GRANDPA key #[cfg_attr(feature = "std", error("Registration with invalid GRANDPA key"))] InvalidGrandpaKey, + /// Registration with missing session key + #[cfg_attr(feature = "std", error("Registration with missing session key"))] + MissingSessionKey, + /// Registration with invalid set of session keys + #[cfg_attr(feature = "std", error("Registration with invalid set of session keys"))] + InvalidSessionKeys, } #[derive(Debug, PartialEq, Eq, Clone, Encode, Decode, TypeInfo)] @@ -231,45 +242,56 @@ pub enum PermissionedCandidateDataError { } /// Validates Aura, GRANDPA, and Partner Chain public keys of [PermissionedCandidateData]. -pub fn validate_permissioned_candidate_data>( +pub fn validate_permissioned_candidate_data< + AuthorityId: TryFrom, + AuthorityKeys: OpaqueKeys + Decode, +>( candidate: PermissionedCandidateData, -) -> Result<(AccountId, sr25519::Public, ed25519::Public), PermissionedCandidateDataError> { - Ok(( - candidate - .sidechain_public_key - .try_into() - .map_err(|_| PermissionedCandidateDataError::InvalidSidechainPubKey)?, - candidate - .aura_public_key - .try_into_sr25519() - .ok_or(PermissionedCandidateDataError::InvalidAuraKey)?, - candidate - .grandpa_public_key - .try_into_ed25519() - .ok_or(PermissionedCandidateDataError::InvalidGrandpaKey)?, - )) +) -> Result<(AuthorityId, AuthorityKeys), PermissionedCandidateDataError> { + unimplemented!() + // match candidate { + // PermissionedCandidateData::V0(data) => Ok(( + // data.sidechain_public_key + // .try_into() + // .map_err(|_| PermissionedCandidateDataError::InvalidSidechainPubKey)?, + // data.aura_public_key + // .try_into_sr25519() + // .ok_or(PermissionedCandidateDataError::InvalidAuraKey)?, + // data.grandpa_public_key + // .try_into_ed25519() + // .ok_or(PermissionedCandidateDataError::InvalidGrandpaKey)?, + // )), + // PermissionedCandidateData::V1(data) => unimplemented!(), + // } } /// Validates registration data provided by the authority candidate. /// /// Validates: -/// * Aura, GRANDPA, and Partner Chain public keys of the candidate +/// * Session and Partner Chain public keys of the candidate /// * stake pool signature /// * sidechain signature /// * transaction inputs contain correct registration utxo -pub fn validate_registration_data( +pub fn validate_registration_data( stake_pool_pub_key: &StakePoolPublicKey, registration_data: &RegistrationData, genesis_utxo: UtxoId, -) -> Result<(ecdsa::Public, (sr25519::Public, ed25519::Public)), RegistrationDataError> { - let aura_pub_key = registration_data - .aura_pub_key - .try_into_sr25519() - .ok_or(RegistrationDataError::InvalidAuraKey)?; - let grandpa_pub_key = registration_data - .grandpa_pub_key - .try_into_ed25519() - .ok_or(RegistrationDataError::InvalidGrandpaKey)?; +) -> Result<(ecdsa::Public, SessionKeys), RegistrationDataError> { + let required_keys = SessionKeys::key_ids(); + + let mut encoded_keys = vec![]; + for key_id in required_keys { + let key = registration_data + .session_keys + .iter() + .find(|key| &KeyTypeId(key.0) == key_id) + .ok_or(RegistrationDataError::MissingSessionKey)?; + encoded_keys.extend(key.1.clone()); + } + + let sessions_keys = SessionKeys::decode(&mut &encoded_keys[..]) + .map_err(|_e| RegistrationDataError::InvalidSessionKeys)?; + let sidechain_pub_key = ecdsa::Public::from( <[u8; 33]>::try_from(registration_data.sidechain_pub_key.0.clone()) .map_err(|_| RegistrationDataError::InvalidSidechainPubKey)?, @@ -292,7 +314,7 @@ pub fn validate_registration_data( )?; verify_tx_inputs(registration_data)?; - Ok((sidechain_pub_key, (aura_pub_key, grandpa_pub_key))) + Ok((sidechain_pub_key, sessions_keys)) } /// Validates stake delegation. Stake must be known and positive. diff --git a/toolkit/committee-selection/authority-selection-inherents/src/select_authorities.rs b/toolkit/committee-selection/authority-selection-inherents/src/select_authorities.rs index fd5a511573..0dbdc3b91f 100644 --- a/toolkit/committee-selection/authority-selection-inherents/src/select_authorities.rs +++ b/toolkit/committee-selection/authority-selection-inherents/src/select_authorities.rs @@ -5,15 +5,17 @@ use crate::filter_invalid_candidates::{ Candidate, filter_invalid_permissioned_candidates, filter_trustless_candidates_registrations, }; use log::{info, warn}; +use parity_scale_codec::Decode; use plutus::*; use sidechain_domain::{EpochNonce, ScEpochNumber, UtxoId}; -use sp_core::{U256, ecdsa, ed25519, sr25519}; +use sp_core::{U256, ecdsa}; +use sp_runtime::traits::OpaqueKeys; /// Selects authorities using the Ariadne selection algorithm and data sourced from Partner Chains smart contracts on Cardano. /// Seed is constructed from the MC epoch nonce and the sidechain epoch. pub fn select_authorities< TAccountId: Clone + Ord + TryFrom + From, - TAccountKeys: Clone + Ord + From<(sr25519::Public, ed25519::Public)>, + TAccountKeys: Clone + Ord + OpaqueKeys + Decode, >( genesis_utxo: UtxoId, input: AuthoritySelectionInputs, diff --git a/toolkit/committee-selection/authority-selection-inherents/src/tests.rs b/toolkit/committee-selection/authority-selection-inherents/src/tests.rs index d6efa627ac..48daca2467 100644 --- a/toolkit/committee-selection/authority-selection-inherents/src/tests.rs +++ b/toolkit/committee-selection/authority-selection-inherents/src/tests.rs @@ -92,12 +92,6 @@ pub struct AccountKeys { pub grandpa: [u8; 32], } -impl From<(sr25519::Public, ed25519::Public)> for AccountKeys { - fn from((aura, grandpa): (sr25519::Public, ed25519::Public)) -> Self { - Self { aura: aura.0, grandpa: grandpa.0 } - } -} - impl AccountKeys { pub fn from_seed(seed: &str) -> AccountKeys { let mut aura = format!("aura-{seed}").into_bytes(); diff --git a/toolkit/committee-selection/query/src/lib.rs b/toolkit/committee-selection/query/src/lib.rs index b57460cd7a..89ed6f852b 100644 --- a/toolkit/committee-selection/query/src/lib.rs +++ b/toolkit/committee-selection/query/src/lib.rs @@ -238,10 +238,22 @@ where let validate_permissioned_candidate = |candidate: &RawPermissionedCandidateData| { api.validate_permissioned_candidate_data( best_block, - sidechain_domain::PermissionedCandidateData { - sidechain_public_key: candidate.sidechain_public_key.clone(), - aura_public_key: candidate.aura_public_key.clone(), - grandpa_public_key: candidate.grandpa_public_key.clone(), + match candidate { + RawPermissionedCandidateData::V0(data) => { + sidechain_domain::PermissionedCandidateDataV0 { + sidechain_public_key: data.sidechain_public_key.clone(), + aura_public_key: data.aura_public_key.clone(), + grandpa_public_key: data.grandpa_public_key.clone(), + } + .into() + }, + RawPermissionedCandidateData::V1(data) => { + sidechain_domain::PermissionedCandidateDataV1 { + keys: data.keys.clone(), + sidechain_key: data.sidechain_key.clone(), + } + .into() + }, }, ) }; diff --git a/toolkit/committee-selection/query/src/types/ariadne.rs b/toolkit/committee-selection/query/src/types/ariadne.rs index ee88df1e3a..f2f33db0b1 100644 --- a/toolkit/committee-selection/query/src/types/ariadne.rs +++ b/toolkit/committee-selection/query/src/types/ariadne.rs @@ -2,7 +2,6 @@ use crate::types::GetRegistrationsResponseMap; use authority_selection_inherents::authority_selection_inputs::RawPermissionedCandidateData; use authority_selection_inherents::filter_invalid_candidates::PermissionedCandidateDataError; use serde::{Deserialize, Serialize}; -use sidechain_domain::{AuraPublicKey, GrandpaPublicKey, SidechainPublicKey}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -41,12 +40,8 @@ impl From for DParameter { #[serde(rename_all = "camelCase")] /// Represents data associated with a permissioned candidate pub struct PermissionedCandidateData { - /// Sidechain public key of the candidate - pub sidechain_public_key: SidechainPublicKey, - /// Aura public key of the candidate - pub aura_public_key: AuraPublicKey, - /// Grandpa public key of the candidate - pub grandpa_public_key: GrandpaPublicKey, + /// Associated data of a candidate + data: RawPermissionedCandidateData, /// Is the registration valid pub is_valid: bool, /// Human-readable reasons of registration being invalid. Present only for invalid entries. @@ -60,12 +55,6 @@ impl PermissionedCandidateData { data: RawPermissionedCandidateData, invalid_reasons: Option, ) -> Self { - Self { - sidechain_public_key: data.sidechain_public_key, - aura_public_key: data.aura_public_key, - grandpa_public_key: data.grandpa_public_key, - is_valid: invalid_reasons.is_none(), - invalid_reasons, - } + Self { data, is_valid: invalid_reasons.is_none(), invalid_reasons } } } diff --git a/toolkit/committee-selection/query/src/types/registrations.rs b/toolkit/committee-selection/query/src/types/registrations.rs index 0bddcda223..a2d3de6c7f 100644 --- a/toolkit/committee-selection/query/src/types/registrations.rs +++ b/toolkit/committee-selection/query/src/types/registrations.rs @@ -37,10 +37,8 @@ pub struct CandidateRegistrationEntry { pub mainchain_pub_key: String, /// Cross chain public key of the candidate. See [sidechain_domain::CrossChainPublicKey] for more details. pub cross_chain_pub_key: String, - /// Aura public key of the candidate - pub aura_pub_key: String, - /// Grandpa public key of the candidate - pub grandpa_pub_key: String, + /// Session public keys of the candidate + pub session_keys: Vec<(String, String)>, /// Sidechain key signature of the registration message pub sidechain_signature: String, /// Signature made with Stake Pool key @@ -79,8 +77,11 @@ impl CandidateRegistrationEntry { .unwrap_or("Invalid Sidechain Public Key. Could not decode...".into()), mainchain_pub_key: to_hex(&stake_pool_public_key.0.clone(), false), cross_chain_pub_key: to_hex(®istration_data.cross_chain_pub_key.0, false), - aura_pub_key: to_hex(®istration_data.aura_pub_key.0, false), - grandpa_pub_key: to_hex(®istration_data.grandpa_pub_key.0, false), + session_keys: registration_data + .session_keys + .iter() + .map(|(key_type, key)| (to_hex(&key_type[..], false), to_hex(&key[..], false))) + .collect(), sidechain_signature: to_hex(®istration_data.sidechain_signature.0, false), mainchain_signature: to_hex(®istration_data.mainchain_signature.0, false), cross_chain_signature: to_hex(®istration_data.cross_chain_signature.0, false), diff --git a/toolkit/data-sources/db-sync/src/candidates/datum.rs b/toolkit/data-sources/db-sync/src/candidates/datum.rs index 5276f21edc..214b255c26 100644 --- a/toolkit/data-sources/db-sync/src/candidates/datum.rs +++ b/toolkit/data-sources/db-sync/src/candidates/datum.rs @@ -1,22 +1,12 @@ use authority_selection_inherents::authority_selection_inputs::RawPermissionedCandidateData; -use partner_chains_plutus_data::permissioned_candidates::PermissionedCandidateDatumV0; use partner_chains_plutus_data::permissioned_candidates::PermissionedCandidateDatums; use sidechain_domain::*; -pub(crate) fn raw_permissioned_candidate_data_from( - datum: PermissionedCandidateDatumV0, -) -> RawPermissionedCandidateData { - let PermissionedCandidateDatumV0 { sidechain_public_key, aura_public_key, grandpa_public_key } = - datum; - RawPermissionedCandidateData { sidechain_public_key, aura_public_key, grandpa_public_key } -} - pub(crate) fn raw_permissioned_candidate_data_vec_from( datums: PermissionedCandidateDatums, ) -> Vec { - match datums { - PermissionedCandidateDatums::V0(datums) => { - datums.into_iter().map(raw_permissioned_candidate_data_from).collect() - }, - } + Vec::::from(datums) + .into_iter() + .map(Into::into) + .collect() } diff --git a/toolkit/data-sources/db-sync/src/candidates/mod.rs b/toolkit/data-sources/db-sync/src/candidates/mod.rs index 2ad3b6c4c6..c8c0549ee0 100644 --- a/toolkit/data-sources/db-sync/src/candidates/mod.rs +++ b/toolkit/data-sources/db-sync/src/candidates/mod.rs @@ -41,8 +41,7 @@ struct RegisteredCandidate { cross_chain_signature: CrossChainSignature, sidechain_pub_key: SidechainPublicKey, cross_chain_pub_key: CrossChainPublicKey, - aura_pub_key: AuraPublicKey, - grandpa_pub_key: GrandpaPublicKey, + session_keys: Vec<([u8; 4], Vec)>, utxo_info: UtxoInfo, } @@ -180,8 +179,7 @@ impl CandidatesDataSourceImpl { cross_chain_signature: c.cross_chain_signature, sidechain_pub_key: c.sidechain_pub_key, cross_chain_pub_key: c.cross_chain_pub_key, - aura_pub_key: c.aura_pub_key, - grandpa_pub_key: c.grandpa_pub_key, + session_keys: c.session_keys, utxo_info: c.utxo_info, tx_inputs: c.tx_inputs, } @@ -220,29 +218,55 @@ impl CandidatesDataSourceImpl { Self::parse_candidates(outputs) .into_iter() .map(|c| { - let RegisterValidatorDatum::V0 { - stake_ownership, - sidechain_pub_key, - sidechain_signature, - registration_utxo, - own_pkh: _own_pkh, - aura_pub_key, - grandpa_pub_key, - } = c.datum; - Ok(RegisteredCandidate { - stake_pool_pub_key: stake_ownership.pub_key, - mainchain_signature: stake_ownership.signature, - // For now we use the same key for both cross chain and sidechain actions - cross_chain_pub_key: CrossChainPublicKey(sidechain_pub_key.0.clone()), - cross_chain_signature: CrossChainSignature(sidechain_signature.0.clone()), - sidechain_signature, - sidechain_pub_key, - aura_pub_key, - grandpa_pub_key, - registration_utxo, - tx_inputs: c.tx_inputs, - utxo_info: c.utxo_info, - }) + match c.datum { + RegisterValidatorDatum::V0 { + stake_ownership, + sidechain_pub_key, + sidechain_signature, + registration_utxo, + own_pkh: _own_pkh, + aura_pub_key, + grandpa_pub_key, + } => { + let session_keys = + vec![(*b"aura", aura_pub_key.0), (*b"gran", grandpa_pub_key.0)]; + Ok(RegisteredCandidate { + stake_pool_pub_key: stake_ownership.pub_key, + mainchain_signature: stake_ownership.signature, + // For now we use the same key for both cross chain and sidechain actions + cross_chain_pub_key: CrossChainPublicKey(sidechain_pub_key.0.clone()), + cross_chain_signature: CrossChainSignature( + sidechain_signature.0.clone(), + ), + sidechain_signature, + sidechain_pub_key, + session_keys, + registration_utxo, + tx_inputs: c.tx_inputs, + utxo_info: c.utxo_info, + }) + }, + RegisterValidatorDatum::V1 { + stake_ownership, + sidechain_pub_key, + sidechain_signature, + registration_utxo, + own_pkh: _own_pkh, + session_keys, + } => Ok(RegisteredCandidate { + stake_pool_pub_key: stake_ownership.pub_key, + mainchain_signature: stake_ownership.signature, + // For now we use the same key for both cross chain and sidechain actions + cross_chain_pub_key: CrossChainPublicKey(sidechain_pub_key.0.clone()), + cross_chain_signature: CrossChainSignature(sidechain_signature.0.clone()), + sidechain_signature, + sidechain_pub_key, + session_keys, + registration_utxo, + tx_inputs: c.tx_inputs, + utxo_info: c.utxo_info, + }), + } }) .collect() } diff --git a/toolkit/data-sources/mock/src/candidate.rs b/toolkit/data-sources/mock/src/candidate.rs index 50f0f2ad51..cba7cdad84 100644 --- a/toolkit/data-sources/mock/src/candidate.rs +++ b/toolkit/data-sources/mock/src/candidate.rs @@ -44,6 +44,9 @@ impl From for CandidateRegistrations { let stake_pool_public_key = StakePoolPublicKey(mock.mainchain_pub_key.0.try_into().expect( "Invalid mock configuration. 'mainchain_pub_key' public key should be 32 bytes.", )); + let session_keys = + vec![(*b"aura", mock.aura_pub_key.0), (*b"gran", mock.grandpa_pub_key.0)]; + let registrations = vec![RegistrationData { registration_utxo: mock.registration_utxo, sidechain_signature: SidechainSignature(mock.sidechain_signature.0.clone()), @@ -66,8 +69,7 @@ impl From for CandidateRegistrations { tx_index_within_block: McTxIndexInBlock(12), }, tx_inputs: vec![mock.registration_utxo], - aura_pub_key: AuraPublicKey(mock.aura_pub_key.0), - grandpa_pub_key: GrandpaPublicKey(mock.grandpa_pub_key.0), + session_keys, }]; let stake_delegation = Some(StakeDelegation(333)); CandidateRegistrations { stake_pool_public_key, registrations, stake_delegation } @@ -108,11 +110,11 @@ impl From for RawPermissionedCandidateData { grandpa_pub_key, }: MockPermissionedCandidate, ) -> Self { - Self { + RawPermissionedCandidateData::V0(RawPermissionedCandidateDataV0 { sidechain_public_key: SidechainPublicKey(sidechain_pub_key.0), aura_public_key: AuraPublicKey(aura_pub_key.0), grandpa_public_key: GrandpaPublicKey(grandpa_pub_key.0), - } + }) } } diff --git a/toolkit/partner-chains-cli/Cargo.toml b/toolkit/partner-chains-cli/Cargo.toml index 638a3804a3..4bb2b9cb68 100644 --- a/toolkit/partner-chains-cli/Cargo.toml +++ b/toolkit/partner-chains-cli/Cargo.toml @@ -17,6 +17,7 @@ workspace = true [dependencies] sidechain-domain = { workspace = true, features = ["std"] } +parity-scale-codec = { workspace = true } anyhow = { workspace = true } clap = { workspace = true } hex = { workspace = true } diff --git a/toolkit/partner-chains-cli/src/create_chain_spec/mod.rs b/toolkit/partner-chains-cli/src/create_chain_spec/mod.rs index 9321941200..cc6c2063e6 100644 --- a/toolkit/partner-chains-cli/src/create_chain_spec/mod.rs +++ b/toolkit/partner-chains-cli/src/create_chain_spec/mod.rs @@ -4,8 +4,10 @@ use crate::permissioned_candidates::{ParsedPermissionedCandidatesKeys, Permissio use crate::runtime_bindings::PartnerChainRuntime; use crate::{CmdRun, config::config_fields}; use anyhow::anyhow; +use parity_scale_codec::Decode; use sidechain_domain::{AssetName, MainchainAddress, PolicyId, UtxoId}; use sp_runtime::DeserializeOwned; +use sp_runtime::traits::OpaqueKeys; use std::marker::PhantomData; #[cfg(test)] @@ -38,7 +40,7 @@ impl CmdRun for CreateChainSpecCmd { } impl CreateChainSpecCmd { - fn print_config(context: &C, config: &CreateChainSpecConfig) { + fn print_config(context: &C, config: &CreateChainSpecConfig) { context.print("Chain parameters:"); context.print(format!("- Genesis UTXO: {}", config.genesis_utxo).as_str()); context.print("SessionValidatorManagement Main Chain Configuration:"); @@ -93,10 +95,11 @@ impl CreateChainSpecCmd { #[allow(missing_docs)] #[derive(Debug)] /// Configuration that contains all Partner Chain specific data required to create the chain spec -pub struct CreateChainSpecConfig { +pub struct CreateChainSpecConfig { pub genesis_utxo: UtxoId, pub initial_permissioned_candidates_raw: Vec, - pub initial_permissioned_candidates_parsed: Vec, + pub initial_permissioned_candidates_parsed: + Vec>, pub committee_candidate_address: MainchainAddress, pub d_parameter_policy_id: PolicyId, pub permissioned_candidates_policy_id: PolicyId, @@ -107,15 +110,17 @@ pub struct CreateChainSpecConfig { pub governed_map_asset_policy_id: Option, } -impl CreateChainSpecConfig { +impl CreateChainSpecConfig { pub(crate) fn load(c: &C) -> Result { let initial_permissioned_candidates_raw = load_config_field(c, &config_fields::INITIAL_PERMISSIONED_CANDIDATES)?; - let initial_permissioned_candidates_parsed: Vec = - initial_permissioned_candidates_raw - .iter() - .map(TryFrom::try_from) - .collect::, anyhow::Error>>()?; + let initial_permissioned_candidates_parsed: Vec< + ParsedPermissionedCandidatesKeys, + > = initial_permissioned_candidates_raw + .iter() + .map(ParsedPermissionedCandidatesKeys::try_from) + .collect::>, anyhow::Error>>( + )?; Ok(Self { genesis_utxo: load_config_field(c, &config_fields::GENESIS_UTXO)?, initial_permissioned_candidates_raw, @@ -154,7 +159,7 @@ impl CreateChainSpecConfig { /// as initial validators pub fn pallet_partner_chains_session_config< T: pallet_partner_chains_session::Config, - F: Fn(&ParsedPermissionedCandidatesKeys) -> (T::ValidatorId, T::Keys), + F: Fn(&ParsedPermissionedCandidatesKeys) -> (T::ValidatorId, T::Keys), >( &self, f: F, @@ -172,7 +177,7 @@ impl CreateChainSpecConfig { /// as initial authorities pub fn pallet_session_validator_management_config< T: pallet_session_validator_management::Config, - F: Fn(&ParsedPermissionedCandidatesKeys) -> T::CommitteeMember, + F: Fn(&ParsedPermissionedCandidatesKeys) -> T::CommitteeMember, >( &self, f: F, diff --git a/toolkit/partner-chains-cli/src/generate_keys/mod.rs b/toolkit/partner-chains-cli/src/generate_keys/mod.rs index 4a12a43182..0267bd8d0b 100644 --- a/toolkit/partner-chains-cli/src/generate_keys/mod.rs +++ b/toolkit/partner-chains-cli/src/generate_keys/mod.rs @@ -110,12 +110,16 @@ pub(crate) fn generate_spo_keys( let aura_key = generate_or_load_key(config, context, &AURA)?; context.enewline(); - let public_keys_json = serde_json::to_string_pretty(&PermissionedCandidateKeys { - sidechain_pub_key: cross_chain_key, - aura_pub_key: aura_key, - grandpa_pub_key: grandpa_key, - }) - .expect("Failed to serialize public keys"); + let keys = PermissionedCandidateKeys { + keys: vec![ + (*b"crch", cross_chain_key.as_bytes().to_vec()), + (*b"aura", aura_key.as_bytes().to_vec()), + (*b"gran", grandpa_key.as_bytes().to_vec()), + ], + }; + + let public_keys_json = + serde_json::to_string_pretty(&keys).expect("Failed to serialize public keys"); context.write_file(KEYS_FILE_PATH, &public_keys_json); context.eprint(&format!( diff --git a/toolkit/partner-chains-cli/src/lib.rs b/toolkit/partner-chains-cli/src/lib.rs index 698b98548c..0f6b1f4098 100644 --- a/toolkit/partner-chains-cli/src/lib.rs +++ b/toolkit/partner-chains-cli/src/lib.rs @@ -59,7 +59,7 @@ pub enum Command { PrepareConfiguration(prepare_configuration::PrepareConfigurationCmd), /// Wizard for setting D-parameter and Permissioned Candidates list on the main chain. /// Uses 'chain config' obtained after running `prepare-configuration`. - SetupMainChainState(setup_main_chain_state::SetupMainChainStateCmd), + SetupMainChainState(setup_main_chain_state::SetupMainChainStateCmd), /// Wizard for creating a chain spec json file based on the chain configuration (see `prepare-configuration`). CreateChainSpec(create_chain_spec::CreateChainSpecCmd), /// Wizard for starting a substrate node in the environment set up by `generate-keys`, diff --git a/toolkit/partner-chains-cli/src/permissioned_candidates.rs b/toolkit/partner-chains-cli/src/permissioned_candidates.rs index a0070a67d3..d123009b73 100644 --- a/toolkit/partner-chains-cli/src/permissioned_candidates.rs +++ b/toolkit/partner-chains-cli/src/permissioned_candidates.rs @@ -1,7 +1,9 @@ use crate::cmd_traits::{GetPermissionedCandidates, UpsertPermissionedCandidates}; +use anyhow::bail; use ogmios_client::query_ledger_state::{QueryLedgerState, QueryUtxoByUtxoId}; use ogmios_client::query_network::QueryNetwork; use ogmios_client::transactions::Transactions; +use parity_scale_codec::Decode; use partner_chains_cardano_offchain::await_tx::FixedDelayRetries; use partner_chains_cardano_offchain::cardano_keys::CardanoPaymentSigningKey; use partner_chains_cardano_offchain::csl::NetworkTypeExt; @@ -12,101 +14,127 @@ use partner_chains_cardano_offchain::permissioned_candidates::{ use serde::{Deserialize, Serialize}; use sidechain_domain::{PermissionedCandidateData, UtxoId}; use sp_core::crypto::AccountId32; -use sp_core::{ecdsa, ed25519, sr25519}; -use sp_runtime::traits::IdentifyAccount; +use sp_core::ecdsa; +use sp_runtime::KeyTypeId; +use sp_runtime::traits::{IdentifyAccount, OpaqueKeys}; use std::fmt::{Display, Formatter}; /// Struct that holds permissioned candidates keys in raw string format #[derive(Debug, Deserialize, Eq, PartialEq, PartialOrd, Ord, Serialize)] pub struct PermissionedCandidateKeys { - /// 0x prefixed hex representation of the ECDSA public key - pub sidechain_pub_key: String, - /// 0x prefixed hex representation of the sr25519 public key - pub aura_pub_key: String, - /// 0x prefixed hex representation of the Ed25519 public key - pub grandpa_pub_key: String, + /// All keys associated with given candidate + pub keys: Vec<([u8; 4], Vec)>, } impl Display for PermissionedCandidateKeys { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "Partner Chains Key: {}, AURA: {}, GRANDPA: {}", - self.sidechain_pub_key, self.aura_pub_key, self.grandpa_pub_key - ) + write!(f, "Keys with key type: {:?}", self.keys) } } impl From<&sidechain_domain::PermissionedCandidateData> for PermissionedCandidateKeys { fn from(value: &sidechain_domain::PermissionedCandidateData) -> Self { - Self { - sidechain_pub_key: sp_core::bytes::to_hex(&value.sidechain_public_key.0, false), - aura_pub_key: sp_core::bytes::to_hex(&value.aura_public_key.0, false), - grandpa_pub_key: sp_core::bytes::to_hex(&value.grandpa_public_key.0, false), + match value { + PermissionedCandidateData::V0(permissioned_candidate_data_v0) => { + PermissionedCandidateKeys { + keys: vec![ + (*b"crch", permissioned_candidate_data_v0.sidechain_public_key.0.clone()), + (*b"aura", permissioned_candidate_data_v0.aura_public_key.0.clone()), + (*b"gran", permissioned_candidate_data_v0.grandpa_public_key.0.clone()), + ], + } + }, + PermissionedCandidateData::V1(permissioned_candidate_data_v1) => { + PermissionedCandidateKeys { keys: permissioned_candidate_data_v1.keys.clone() } + }, } } } -/// Groups together keys of permissioned candidates. Expected to turn into a more generic type. +/// Groups together keys of permissioned candidates. #[derive(Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)] -pub struct ParsedPermissionedCandidatesKeys { - /// Polkadot identity of the permissioned candidate (aka. partner-chain key) - pub sidechain: ecdsa::Public, - /// AURA key of the permissioned candidate - pub aura: sr25519::Public, - /// Grandpa key of the permissioned candidate - pub grandpa: ed25519::Public, +pub struct ParsedPermissionedCandidatesKeys { + sidechain_key: ecdsa::Public, + keys: AuthorityKeys, } -impl ParsedPermissionedCandidatesKeys { +impl ParsedPermissionedCandidatesKeys { + /// Permissioned candidate set of session keys + pub fn session_keys(&self) -> &AuthorityKeys { + &self.keys + } + + /// Permissioned Candidate partner-chain (sidechain) key + pub fn sidechain_key(&self) -> ecdsa::Public { + self.sidechain_key + } + /// Permissioned Candidate partner-chain (sidechain) key mapped to AccountId32 pub fn account_id_32(&self) -> AccountId32 { - sp_runtime::MultiSigner::from(self.sidechain).into_account() + sp_runtime::MultiSigner::from(self.sidechain_key()).into_account() } } -impl TryFrom<&PermissionedCandidateKeys> for ParsedPermissionedCandidatesKeys { +impl TryFrom<&PermissionedCandidateKeys> + for ParsedPermissionedCandidatesKeys +{ type Error = anyhow::Error; fn try_from(value: &PermissionedCandidateKeys) -> Result { - let sidechain = parse_ecdsa(&value.sidechain_pub_key).ok_or(anyhow::Error::msg( - format!("{} is invalid ECDSA public key", value.sidechain_pub_key), - ))?; - let aura = parse_sr25519(&value.aura_pub_key).ok_or(anyhow::Error::msg(format!( - "{} is invalid sr25519 public key", - value.aura_pub_key - )))?; - let grandpa = parse_ed25519(&value.grandpa_pub_key).ok_or(anyhow::Error::msg(format!( - "{} is invalid Ed25519 public key", - value.grandpa_pub_key - )))?; - Ok(Self { sidechain, aura, grandpa }) - } -} + let expected_keys = AuthorityKeys::key_ids(); -impl From<&ParsedPermissionedCandidatesKeys> for sidechain_domain::PermissionedCandidateData { - fn from(value: &ParsedPermissionedCandidatesKeys) -> Self { - Self { - sidechain_public_key: sidechain_domain::SidechainPublicKey(value.sidechain.0.to_vec()), - aura_public_key: sidechain_domain::AuraPublicKey(value.aura.0.to_vec()), - grandpa_public_key: sidechain_domain::GrandpaPublicKey(value.grandpa.0.to_vec()), + if expected_keys.len() != value.keys.len() + 1 { + bail!( + "Invalid number of keys, expeced sidechain key and {} session keys, provided {} session keys", + expected_keys.len(), + value.keys.len() + ); } - } -} -fn parse_ecdsa(value: &str) -> Option { - let bytes = sp_core::bytes::from_hex(value).ok()?; - Some(ecdsa::Public::from(<[u8; 33]>::try_from(bytes).ok()?)) -} + let sidechain_key = { + // TODO: we are not verifying if there is only one crch key, also try to aggregate 2 steps into one + let (_sidechain_key_type, sidechain_key) = value + .keys + .iter() + .find(|key| key.0 == *b"crch") + .ok_or(anyhow::Error::msg(format!("Missing ECDSA sidechain key"))) + .cloned()?; + + let sidechain_key = <[u8; 33]>::try_from(sidechain_key).map_err(|sidechain_key| { + anyhow::Error::msg(format!("{:?} is invalid ECDSA public key", sidechain_key)) + })?; + + let sidechain_key = ecdsa::Public::from(sidechain_key).into(); + sidechain_key + }; + + let keys = { + let mut encoded_keys = vec![]; + + // TODO: we are not handling duplicates or reordering, question rather if we should allow for it due we can handle it there + for key_type in expected_keys { + let key = value.keys.iter().find(|key| &KeyTypeId(key.0) == key_type).ok_or_else( + || anyhow::Error::msg(format!("Missing session key {:?}", key_type)), + )?; + encoded_keys.extend(key.1.clone()); + } -fn parse_sr25519(value: &str) -> Option { - let bytes = sp_core::bytes::from_hex(value).ok()?; - Some(sr25519::Public::from(<[u8; 32]>::try_from(bytes).ok()?)) + let sessions_keys = AuthorityKeys::decode(&mut &encoded_keys[..]) + .map_err(|_e| anyhow::Error::msg(format!("Invalid session keys")))?; + + sessions_keys + }; + + Ok(Self { sidechain_key, keys }) + } } -fn parse_ed25519(value: &str) -> Option { - let bytes = sp_core::bytes::from_hex(value).ok()?; - Some(ed25519::Public::from(<[u8; 32]>::try_from(bytes).ok()?)) +impl From<&ParsedPermissionedCandidatesKeys> + for sidechain_domain::PermissionedCandidateData +{ + fn from(value: &ParsedPermissionedCandidatesKeys) -> Self { + value.into() + } } impl diff --git a/toolkit/partner-chains-cli/src/register/register3.rs b/toolkit/partner-chains-cli/src/register/register3.rs index ecad09f40a..da84752864 100644 --- a/toolkit/partner-chains-cli/src/register/register3.rs +++ b/toolkit/partner-chains-cli/src/register/register3.rs @@ -63,8 +63,11 @@ impl CmdRun for Register3Cmd { partner_chain_signature: self.partner_chain_signature.clone(), own_pkh: payment_signing_key.to_pub_key_hash(), registration_utxo: self.registration_utxo, - aura_pub_key: self.aura_pub_key.clone(), - grandpa_pub_key: self.grandpa_pub_key.clone(), + // TODO: FIX me + session_keys: vec![ + (*b"aura", self.aura_pub_key.0.clone()), + (*b"gran", self.grandpa_pub_key.0.clone()), + ], }; let offchain = context.offchain_impl(&ogmios_configuration)?; diff --git a/toolkit/partner-chains-cli/src/runtime_bindings.rs b/toolkit/partner-chains-cli/src/runtime_bindings.rs index 9ec390dd17..fc1941034f 100644 --- a/toolkit/partner-chains-cli/src/runtime_bindings.rs +++ b/toolkit/partner-chains-cli/src/runtime_bindings.rs @@ -1,4 +1,8 @@ use crate::CreateChainSpecConfig; +use parity_scale_codec::Decode; +use serde::Serialize; +use sp_core::ecdsa; +use sp_runtime::traits::OpaqueKeys; /// Trait wrapping Substrate runtime type. Should be implemented for the runtime of the node. pub trait RuntimeTypeWrapper { @@ -8,6 +12,15 @@ pub trait RuntimeTypeWrapper { /// Trait defining Partner Chain governance related types. pub trait PartnerChainRuntime { + /// Partner Chain authority id type + type AuthorityId: Send + Sync + 'static + From; + /// Partner Chain authority key type + type AuthorityKeys: Send + Sync + OpaqueKeys + Serialize + Decode; + /// Partner Chain committee member type + type CommitteeMember: Serialize; + + /// Should construct initial [CommitteeMember] of the chain. Used for creating chain spec. + fn initial_member(id: Self::AuthorityId, keys: Self::AuthorityKeys) -> Self::CommitteeMember; /// User defined function to create a chain spec given the PC configuration - fn create_chain_spec(config: &CreateChainSpecConfig) -> serde_json::Value; + fn create_chain_spec(config: &CreateChainSpecConfig) -> serde_json::Value; } diff --git a/toolkit/partner-chains-cli/src/setup_main_chain_state/mod.rs b/toolkit/partner-chains-cli/src/setup_main_chain_state/mod.rs index df589ae6b8..a5a1727f54 100644 --- a/toolkit/partner-chains-cli/src/setup_main_chain_state/mod.rs +++ b/toolkit/partner-chains-cli/src/setup_main_chain_state/mod.rs @@ -1,3 +1,5 @@ +use std::marker::PhantomData; + use crate::cmd_traits::{ GetDParam, GetPermissionedCandidates, UpsertDParam, UpsertPermissionedCandidates, }; @@ -6,12 +8,13 @@ use crate::config::{ChainConfig, ConfigFieldDefinition, ConfigFile, config_field use crate::io::IOContext; use crate::ogmios::config::prompt_ogmios_configuration; use crate::permissioned_candidates::{ParsedPermissionedCandidatesKeys, PermissionedCandidateKeys}; -use crate::{CmdRun, cardano_key}; +use crate::{CmdRun, PartnerChainRuntime, cardano_key}; use anyhow::Context; use anyhow::anyhow; use ogmios_client::query_ledger_state::{QueryLedgerState, QueryUtxoByUtxoId}; use ogmios_client::query_network::QueryNetwork; use ogmios_client::transactions::Transactions; +use parity_scale_codec::Decode; use partner_chains_cardano_offchain::await_tx::FixedDelayRetries; use partner_chains_cardano_offchain::cardano_keys::CardanoPaymentSigningKey; use partner_chains_cardano_offchain::d_param::{get_d_param, upsert_d_param}; @@ -20,25 +23,37 @@ use partner_chains_cardano_offchain::multisig::{ }; use serde::de::DeserializeOwned; use sidechain_domain::{DParameter, PermissionedCandidateData, UtxoId}; +use sp_runtime::traits::OpaqueKeys; #[cfg(test)] mod tests; #[derive(Clone, Debug, clap::Parser)] -pub struct SetupMainChainStateCmd { +pub struct SetupMainChainStateCmd { #[clap(flatten)] common_arguments: crate::CommonArguments, + #[clap(skip)] + _phantom: PhantomData, } -impl TryFrom for ParsedPermissionedCandidatesKeys { +impl TryFrom + for ParsedPermissionedCandidatesKeys +{ type Error = anyhow::Error; fn try_from(value: PermissionedCandidateData) -> Result { - let keys = PermissionedCandidateKeys { - sidechain_pub_key: hex::encode(value.sidechain_public_key.0), - aura_pub_key: hex::encode(value.aura_public_key.0), - grandpa_pub_key: hex::encode(value.grandpa_public_key.0), + let keys = match value { + PermissionedCandidateData::V0(v0) => PermissionedCandidateKeys { + keys: vec![ + // TODO: is hex::encode needed? + (*b"crch", v0.sidechain_public_key.0), + (*b"aura", v0.aura_public_key.0), + (*b"gran", v0.grandpa_public_key.0), + ], + }, + PermissionedCandidateData::V1(v1) => PermissionedCandidateKeys { keys: v1.keys }, }; + TryFrom::try_from(&keys) } } @@ -53,14 +68,14 @@ impl SortedPermissionedCandidates { } } -impl CmdRun for SetupMainChainStateCmd { +impl CmdRun for SetupMainChainStateCmd { fn run(&self, context: &C) -> anyhow::Result<()> { let chain_config = crate::config::load_chain_config(context)?; context.print( "This wizard will set or update D-Parameter and Permissioned Candidates on the main chain. Setting either of these costs ADA!", ); let config_initial_authorities = - initial_permissioned_candidates_from_chain_config(context)?; + initial_permissioned_candidates_from_chain_config::(context)?; context.print("Will read the current D-Parameter and Permissioned Candidates from the main chain using Ogmios client."); let ogmios_config = prompt_ogmios_configuration(context)?; let offchain = context.offchain_impl(&ogmios_config)?; @@ -102,7 +117,10 @@ impl CmdRun for SetupMainChainStateCmd { } } -fn initial_permissioned_candidates_from_chain_config( +fn initial_permissioned_candidates_from_chain_config< + C: IOContext, + AuthorityKeys: OpaqueKeys + Decode, +>( context: &C, ) -> anyhow::Result { // Requirements state "read from 'chain config' (or chain-spec). @@ -113,7 +131,7 @@ fn initial_permissioned_candidates_from_chain_config( let candidates = candidates .iter() .map(ParsedPermissionedCandidatesKeys::try_from) - .collect::, _>>()?; + .collect::>, _>>()?; let candidates = candidates.iter().map(PermissionedCandidateData::from).collect(); Ok(SortedPermissionedCandidates::new(candidates)) } diff --git a/toolkit/sidechain/domain/src/lib.rs b/toolkit/sidechain/domain/src/lib.rs index 735626eb8a..a204b19509 100644 --- a/toolkit/sidechain/domain/src/lib.rs +++ b/toolkit/sidechain/domain/src/lib.rs @@ -1021,10 +1021,8 @@ pub struct RegistrationData { pub utxo_info: UtxoInfo, /// List of inputs to the registration transaction pub tx_inputs: Vec, - /// Registering SPO's Aura public key - pub aura_pub_key: AuraPublicKey, - /// Registering SPO's Grandpa public key - pub grandpa_pub_key: GrandpaPublicKey, + /// Registering SPO's Session public keys with key type identifiers + pub session_keys: Vec<([u8; 4], Vec)>, } /// Information about an Authority Candidate's Registrations at some block. @@ -1150,7 +1148,57 @@ impl DParameter { /// Permissioned candidates are nominated by the Partner Chain's governance authority to be /// eligible for participation in block producer committee without controlling any ADA stake /// on Cardano and registering as SPOs. -pub struct PermissionedCandidateData { +pub enum PermissionedCandidateData { + /// Initial/legacy permissioned candidate schema + V0(PermissionedCandidateDataV0), + /// Permissioned candidate with universal keys + V1(PermissionedCandidateDataV1), +} + +impl PermissionedCandidateData { + /// Provides associated cross chain key + pub fn sidechain_key(&self) -> Vec { + match self { + Self::V0(value) => value.sidechain_public_key.0.clone(), + // TODO: convert me to ecdsa::Public + Self::V1(value) => value.sidechain_key.to_vec(), + } + } + + /// Provides associated list of session keys + pub fn session_keys(&self) -> Vec<([u8; 4], Vec)> { + match self { + Self::V0(value) => { + vec![ + (*b"crch", value.sidechain_public_key.0.clone()), + (*b"aura", value.aura_public_key.0.clone()), + (*b"gran", value.grandpa_public_key.0.clone()), + ] + }, + Self::V1(value) => value.keys.clone(), + } + } +} + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + Decode, + DecodeWithMemTracking, + Encode, + TypeInfo, + PartialOrd, + Ord, + Hash, +)] +/// Information about a permissioned committee member candidate +/// +/// Permissioned candidates are nominated by the Partner Chain's governance authority to be +/// eligible for participation in block producer committee without controlling any ADA stake +/// on Cardano and registering as SPOs. +pub struct PermissionedCandidateDataV0 { /// Sidechain public key of the trustless candidate pub sidechain_public_key: SidechainPublicKey, /// Aura public key of the trustless candidate @@ -1159,6 +1207,43 @@ pub struct PermissionedCandidateData { pub grandpa_public_key: GrandpaPublicKey, } +#[derive( + Debug, + Clone, + PartialEq, + Eq, + Decode, + DecodeWithMemTracking, + Encode, + TypeInfo, + PartialOrd, + Ord, + Hash, +)] +/// Information about a permissioned committee member candidate +/// +/// Permissioned candidates are nominated by the Partner Chain's governance authority to be +/// eligible for participation in block producer committee without controlling any ADA stake +/// on Cardano and registering as SPOs. +pub struct PermissionedCandidateDataV1 { + /// Cross chain identifier key + pub sidechain_key: ecdsa::Public, + /// Arbitrary set of keys with associated key type + pub keys: Vec<([u8; 4], Vec)>, +} + +impl From for PermissionedCandidateData { + fn from(value: PermissionedCandidateDataV0) -> Self { + PermissionedCandidateData::V0(value) + } +} + +impl From for PermissionedCandidateData { + fn from(value: PermissionedCandidateDataV1) -> Self { + PermissionedCandidateData::V1(value) + } +} + /// Cardano SPO registration. This is a stripped-down version of [RegistrationData]. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct CandidateRegistration { @@ -1172,10 +1257,8 @@ pub struct CandidateRegistration { pub own_pkh: MainchainKeyHash, /// UTxO containing the registration data pub registration_utxo: UtxoId, - /// Registering SPO's Aura public key - pub aura_pub_key: AuraPublicKey, - /// Registering SPO's Grandpa public key - pub grandpa_pub_key: GrandpaPublicKey, + /// Session keys of a candidate + pub session_keys: Vec<([u8; 4], Vec)>, } impl CandidateRegistration { @@ -1184,8 +1267,8 @@ impl CandidateRegistration { self.stake_ownership == other.stake_ownership && self.partner_chain_pub_key == other.partner_chain_pub_key && self.partner_chain_signature == other.partner_chain_signature - && self.aura_pub_key == other.aura_pub_key - && self.grandpa_pub_key == other.grandpa_pub_key + // TODO: compared but without checking order/uniques + && self.session_keys == other.session_keys } } diff --git a/toolkit/smart-contracts/commands/Cargo.toml b/toolkit/smart-contracts/commands/Cargo.toml index a15e36254a..11d491f244 100644 --- a/toolkit/smart-contracts/commands/Cargo.toml +++ b/toolkit/smart-contracts/commands/Cargo.toml @@ -11,9 +11,10 @@ repository.workspace = true workspace = true [dependencies] -clap = { workspace = true, features = [ "derive", "env" ] } +clap = { workspace = true, features = ["derive", "env"] } sidechain-domain = { workspace = true } partner-chains-cardano-offchain = { workspace = true } +sp-core = { workspace = true } hex = { workspace = true } ogmios-client = { workspace = true } tokio = { workspace = true } diff --git a/toolkit/smart-contracts/commands/src/lib.rs b/toolkit/smart-contracts/commands/src/lib.rs index ea838cac64..130ff3703f 100644 --- a/toolkit/smart-contracts/commands/src/lib.rs +++ b/toolkit/smart-contracts/commands/src/lib.rs @@ -31,6 +31,7 @@ use partner_chains_cardano_offchain::{ }; use serde::Serialize; use sidechain_domain::*; +use sp_core::ecdsa; use std::time::Duration; pub mod assemble_tx; @@ -181,22 +182,47 @@ impl From for UtxoId { } } -// Parses public keys in formatted as SIDECHAIN_KEY:AURA_KEY:GRANDPA_KEY +// Parses public key pair in a format keyt:KEY +fn try_parse_public_key(key_pair: &str) -> CmdResult<([u8; 4], Vec)> { + let [key_type, key] = key_pair + .split(":") + .collect::>() + .try_into() + .map_err(|e| format!("Expected key with identifier, keyt:KEY, {:?}", e))?; + + let key_type: [u8; 4] = key_type + .as_bytes() + .try_into() + .map_err(|e| format!("Expected 4 character identifier. {:?}", e))?; + let key = hex::decode(key)?; + + Ok((key_type, key)) +} + +// Parses public keys formatted as crch:SIDECHAIN_KEY,aura:AURA_KEY,gran:GRANDPA_KEY pub(crate) fn parse_partnerchain_public_keys( partner_chain_public_keys: &str, ) -> CmdResult { let partner_chain_public_keys = partner_chain_public_keys.replace("0x", ""); - if let [sidechain_pub_key, aura_pub_key, grandpa_pub_key] = - partner_chain_public_keys.split(":").collect::>()[..] - { - Ok(PermissionedCandidateData { - sidechain_public_key: SidechainPublicKey(hex::decode(sidechain_pub_key)?), - aura_public_key: AuraPublicKey(hex::decode(aura_pub_key)?), - grandpa_public_key: GrandpaPublicKey(hex::decode(grandpa_pub_key)?), - }) - } else { - Err("Failed to parse partner chain public keys.".into()) - } + + let keys = partner_chain_public_keys + .split(',') + .map(try_parse_public_key) + .collect::>>()?; + + let (_sidechain_key_type, sidechain_key) = keys + .iter() + .find(|key| key.0 == *b"crch") + .ok_or(anyhow::Error::msg(format!("Missing ECDSA sidechain key"))) + .cloned()?; + + let sidechain_key = <[u8; 33]>::try_from(sidechain_key).map_err(|sidechain_key| { + anyhow::Error::msg(format!("{:?} is invalid ECDSA public key", sidechain_key)) + })?; + + let sidechain_key = ecdsa::Public::from(sidechain_key).into(); + + Ok(PermissionedCandidateDataV1 { keys, sidechain_key }.into()) } #[cfg(test)] diff --git a/toolkit/smart-contracts/commands/src/register.rs b/toolkit/smart-contracts/commands/src/register.rs index f1b3bf1098..863e1cb47a 100644 --- a/toolkit/smart-contracts/commands/src/register.rs +++ b/toolkit/smart-contracts/commands/src/register.rs @@ -51,12 +51,13 @@ impl RegisterCmd { pub_key: self.spo_public_key, signature: self.spo_signature, }, - partner_chain_pub_key: self.partner_chain_public_keys.sidechain_public_key, + partner_chain_pub_key: sidechain_domain::SidechainPublicKey( + self.partner_chain_public_keys.sidechain_key(), + ), partner_chain_signature: self.partner_chain_signature, own_pkh: payment_key.to_pub_key_hash(), registration_utxo: self.registration_utxo, - aura_pub_key: self.partner_chain_public_keys.aura_public_key, - grandpa_pub_key: self.partner_chain_public_keys.grandpa_public_key, + session_keys: self.partner_chain_public_keys.session_keys(), }; let result = run_register( diff --git a/toolkit/smart-contracts/plutus-data/Cargo.toml b/toolkit/smart-contracts/plutus-data/Cargo.toml index ce0fd6969c..82ffb19425 100644 --- a/toolkit/smart-contracts/plutus-data/Cargo.toml +++ b/toolkit/smart-contracts/plutus-data/Cargo.toml @@ -14,6 +14,7 @@ workspace = true [dependencies] log = { workspace = true } sidechain-domain = { workspace = true, features = ["std"] } +sp-core = { workspace = true } cardano-serialization-lib = { workspace = true } thiserror = { workspace = true } @@ -21,5 +22,6 @@ thiserror = { workspace = true } [dev-dependencies] raw-scripts = { workspace = true } serde_json = { workspace = true } +hex = { workspace = true } hex-literal = { workspace = true } pretty_assertions = { workspace = true } diff --git a/toolkit/smart-contracts/plutus-data/src/permissioned_candidates.rs b/toolkit/smart-contracts/plutus-data/src/permissioned_candidates.rs index 6b57a116dd..38dccb4f13 100644 --- a/toolkit/smart-contracts/plutus-data/src/permissioned_candidates.rs +++ b/toolkit/smart-contracts/plutus-data/src/permissioned_candidates.rs @@ -1,6 +1,7 @@ //! Plutus data types for permissioned candidates. use cardano_serialization_lib::{BigNum, PlutusData, PlutusList}; use sidechain_domain::*; +use sp_core::ecdsa; use crate::{ DataDecodingError, DecodingResult, VersionedDatum, VersionedDatumWithLegacy, @@ -12,6 +13,8 @@ use crate::{ pub enum PermissionedCandidateDatums { /// Initial/legacy datum schema. If a datum doesn't contain a version, it is assumed to be V0 V0(Vec), + /// Schema with generic set of keys + V1(Vec), } #[derive(Clone, Debug, PartialEq)] @@ -34,18 +37,36 @@ impl TryFrom for PermissionedCandidateDatums { impl From for PermissionedCandidateData { fn from(value: PermissionedCandidateDatumV0) -> Self { - Self { + Self::V0(PermissionedCandidateDataV0 { sidechain_public_key: value.sidechain_public_key, aura_public_key: value.aura_public_key, grandpa_public_key: value.grandpa_public_key, - } + }) + } +} + +impl From for PermissionedCandidateData { + fn from(value: PermissionedCandidateDatumV1) -> Self { + let PermissionedCandidateDatumV1 { sidechain_key, keys } = value; + + Self::V1(PermissionedCandidateDataV1 { sidechain_key, keys }) } } +#[derive(Clone, Debug, PartialEq)] +/// Datum representing a premissioned candidate with arbitraty set of keys +pub struct PermissionedCandidateDatumV1 { + /// Cross chain identifier key + pub sidechain_key: ecdsa::Public, + /// Represents arbitrary set of keys with 4 character identifier + pub keys: Vec<([u8; 4], Vec)>, +} + impl From for Vec { fn from(value: PermissionedCandidateDatums) -> Self { match value { PermissionedCandidateDatums::V0(v) => v.into_iter().map(|d| d.into()).collect(), + PermissionedCandidateDatums::V1(v) => v.into_iter().map(|d| d.into()).collect(), } } } @@ -57,38 +78,67 @@ impl From for Vec { /// - datum: () /// - appendix: /// [ -/// [ candidates[0].sidechain_public_key -/// , candidates[0].aura_public_key -/// , candidates[0].grandpa_public_key -/// ] -/// , -/// [ candidates[1].sidechain_public_key -/// , candidates[1].aura_public_key -/// , candidates[1].grandpa_public_key +/// [ +/// [b"crch", candidates[0].sidechain_public_key], +/// [b"aura", candidates[0].aura_public_key], +/// [b"gran", candidates[0].grandpa_public_key] +/// ], +/// [ +/// [b"crch", candidates[1].sidechain_public_key], +/// [b"aura", candidates[1].aura_public_key], +/// [b"gran", candidates[1].grandpa_public_key] /// ] /// // etc. /// ] -/// - version: 0 +/// - version: 1 pub fn permissioned_candidates_to_plutus_data( candidates: &[PermissionedCandidateData], ) -> PlutusData { let mut list = PlutusList::new(); for candidate in candidates { let mut candidate_datum = PlutusList::new(); - candidate_datum.add(&PlutusData::new_bytes(candidate.sidechain_public_key.0.clone())); - candidate_datum.add(&PlutusData::new_bytes(candidate.aura_public_key.0.clone())); - candidate_datum.add(&PlutusData::new_bytes(candidate.grandpa_public_key.0.clone())); + match candidate { + PermissionedCandidateData::V0(data) => { + candidate_datum.add(&PlutusData::new_bytes(data.sidechain_public_key.0.clone())); + candidate_datum.add(&PlutusData::new_bytes(data.aura_public_key.0.clone())); + candidate_datum.add(&PlutusData::new_bytes(data.grandpa_public_key.0.clone())); + }, + PermissionedCandidateData::V1(data) => { + for (key_type, key) in &data.keys { + let mut key_type_with_key = PlutusList::new(); + key_type_with_key.add(&PlutusData::new_bytes(key_type.to_vec())); + key_type_with_key.add(&PlutusData::new_bytes(key.to_vec())); + candidate_datum.add(&PlutusData::new_list(&key_type_with_key)); + } + }, + }; list.add(&PlutusData::new_list(&candidate_datum)); } let appendix = PlutusData::new_list(&list); VersionedGenericDatum { datum: PlutusData::new_empty_constr_plutus_data(&BigNum::zero()), appendix, - version: 0, + version: 1, } .into() } +impl PermissionedCandidateDatums { + /// Parses plutus data schema in accordance with V1 schema + fn decode_v1(data: &PlutusData) -> Result { + let permissioned_candidates = data + .as_list() + .and_then(|list_datums| { + list_datums + .into_iter() + .map(decode_v1_candidate_datum) + .collect::>>() + }) + .ok_or("Expected [[ByteString, ByteString], [ByteString, ByteString], ... ]")?; + Ok(Self::V1(permissioned_candidates)) + } +} + impl VersionedDatumWithLegacy for PermissionedCandidateDatums { const NAME: &str = "PermissionedCandidateDatums"; @@ -115,6 +165,8 @@ impl VersionedDatumWithLegacy for PermissionedCandidateDatums { match version { 0 => PermissionedCandidateDatums::decode_legacy(appendix) .map_err(|msg| format!("Cannot parse appendix: {msg}")), + 1 => PermissionedCandidateDatums::decode_v1(appendix) + .map_err(|msg| format!("Cannot parse appendix: {msg}")), _ => Err(format!("Unknown version: {version}")), } } @@ -134,12 +186,27 @@ fn decode_legacy_candidate_datum(datum: &PlutusData) -> Option Option { + let keys: Vec<([u8; 4], Vec)> = datum + .as_list() + .iter() + .map(|datum| { + (datum.get(0).as_bytes().unwrap().try_into().unwrap(), datum.get(1).as_bytes().unwrap()) + }) + .collect(); + + let sidechain_key = keys.iter().find(|(key_type, _key)| key_type == b"crch")?.1.clone(); + let sidechain_key = <[u8; 33]>::try_from(sidechain_key).ok()?; + let sidechain_key = ecdsa::Public::try_from(sidechain_key).ok()?; + + Some(PermissionedCandidateDatumV1 { keys, sidechain_key }) +} + #[cfg(test)] -mod tests { +mod plutus_data { use super::*; use crate::test_helpers::*; use hex_literal::hex; - use pretty_assertions::assert_eq; #[test] fn valid_legacy_permissioned_candidates() { @@ -200,7 +267,44 @@ mod tests { {"bytes": "7392f3ea668aa2be7997d82c07bcfbec3ee4a9a4e01e3216d92b8f0d0a086c32"} ]} ]}, - { "int": 0 } + { "int": 1 } + ] + }) + } + + fn v1_datum_json() -> serde_json::Value { + serde_json::json!({ + "list": [ + { "constructor": 0, "fields": [] }, + {"list": [ + {"list": [ + {"bytes": hex::encode(b"crch")}, + {"bytes": "cb6df9de1efca7a3998a8ead4e02159d5fa99c3e0d4fd6432667390bb4726854"} + ]}, + {"list": [ + {"bytes": hex::encode(b"aura")}, + {"bytes": "bf20afa1c1a72af3341fa7a447e3f9eada9f3d054a7408fb9e49ad4d6e6559ec"} + ]}, + {"list": [ + {"bytes": hex::encode(b"gran")}, + {"bytes": "9042a40b0b1baa9adcead024432a923eac706be5e1a89d7f2f2d58bfa8f3c26d"} + ]} + ]}, + {"list": [ + {"list": [ + {"bytes": hex::encode(b"crch")}, + {"bytes": "79c3b7fc0b7697b9414cb87adcb37317d1cab32818ae18c0e97ad76395d1fdcf"} + ]}, + {"list": [ + {"bytes": hex::encode(b"aura")}, + {"bytes": "56d1da82e56e4cb35b13de25f69a3e9db917f3e13d6f786321f4b0a9dc153b19"} + ]}, + {"list": [ + {"bytes": hex::encode(b"gran")}, + {"bytes": "7392f3ea668aa2be7997d82c07bcfbec3ee4a9a4e01e3216d92b8f0d0a086c32"} + ]} + ]}, + { "int": 1 } ] }) } @@ -210,7 +314,7 @@ mod tests { let expected_plutus_data = json_to_plutus_data(v0_datum_json()); let domain_data = vec![ - PermissionedCandidateData { + PermissionedCandidateData::V0(PermissionedCandidateDataV0 { sidechain_public_key: SidechainPublicKey( hex!("cb6df9de1efca7a3998a8ead4e02159d5fa99c3e0d4fd6432667390bb4726854") .to_vec(), @@ -223,8 +327,8 @@ mod tests { hex!("9042a40b0b1baa9adcead024432a923eac706be5e1a89d7f2f2d58bfa8f3c26d") .to_vec(), ), - }, - PermissionedCandidateData { + }), + PermissionedCandidateData::V0(PermissionedCandidateDataV0 { sidechain_public_key: SidechainPublicKey( hex!("79c3b7fc0b7697b9414cb87adcb37317d1cab32818ae18c0e97ad76395d1fdcf") .to_vec(), @@ -237,7 +341,7 @@ mod tests { hex!("7392f3ea668aa2be7997d82c07bcfbec3ee4a9a4e01e3216d92b8f0d0a086c32") .to_vec(), ), - }, + }), ]; assert_eq!(permissioned_candidates_to_plutus_data(&domain_data), expected_plutus_data) } @@ -273,4 +377,52 @@ mod tests { assert_eq!(PermissionedCandidateDatums::try_from(plutus_data).unwrap(), expected_datum) } + + #[test] + fn valid_v1_permissioned_candidates() { + let plutus_data = json_to_plutus_data(v1_datum_json()); + + let expected_datum = PermissionedCandidateDatums::V1(vec![ + PermissionedCandidateDatumV1 { + keys: vec![ + ( + *b"crch", + hex!("cb6df9de1efca7a3998a8ead4e02159d5fa99c3e0d4fd6432667390bb4726854") + .into(), + ), + ( + *b"aura", + hex!("bf20afa1c1a72af3341fa7a447e3f9eada9f3d054a7408fb9e49ad4d6e6559ec") + .into(), + ), + ( + *b"gran", + hex!("9042a40b0b1baa9adcead024432a923eac706be5e1a89d7f2f2d58bfa8f3c26d") + .into(), + ), + ], + }, + PermissionedCandidateDatumV1 { + keys: vec![ + ( + *b"crch", + hex!("79c3b7fc0b7697b9414cb87adcb37317d1cab32818ae18c0e97ad76395d1fdcf") + .into(), + ), + ( + *b"aura", + hex!("56d1da82e56e4cb35b13de25f69a3e9db917f3e13d6f786321f4b0a9dc153b19") + .into(), + ), + ( + *b"gran", + hex!("7392f3ea668aa2be7997d82c07bcfbec3ee4a9a4e01e3216d92b8f0d0a086c32") + .into(), + ), + ], + }, + ]); + + assert_eq!(PermissionedCandidateDatums::try_from(plutus_data).unwrap(), expected_datum) + } } diff --git a/toolkit/smart-contracts/plutus-data/src/registered_candidates.rs b/toolkit/smart-contracts/plutus-data/src/registered_candidates.rs index 889daaaa38..58b58a9914 100644 --- a/toolkit/smart-contracts/plutus-data/src/registered_candidates.rs +++ b/toolkit/smart-contracts/plutus-data/src/registered_candidates.rs @@ -56,6 +56,23 @@ pub enum RegisterValidatorDatum { /// Registering SPO's GRANDPA public key grandpa_pub_key: GrandpaPublicKey, }, + /// V1 datum with support for generic session keys + V1 { + /// Stake ownership information of registered candidate. + stake_ownership: AdaBasedStaking, + /// Sidechain public key of the candidate. See [SidechainPublicKey] for more details. + sidechain_pub_key: SidechainPublicKey, + /// Sidechain key signature of the registration message. + sidechain_signature: SidechainSignature, + /// UTxO id that is a part of the signed registration message. + /// It is spent during the registration process. Prevents replay attacks. + registration_utxo: UtxoId, + /// Hash of the registering SPO's Cardano public key. + /// Used by offchain code to find the registration UTXO when re-registering or de-registering. + own_pkh: MainchainKeyHash, + /// Session keys of registered candidate + session_keys: Vec<([u8; 4], Vec)>, + }, } impl TryFrom for RegisterValidatorDatum { @@ -81,23 +98,24 @@ impl VersionedDatumWithLegacy for RegisterValidatorDatum { match version { 0 => decode_v0_register_validator_datum(datum, appendix) .ok_or("Can not parse appendix".to_string()), + 1 => decode_v1_register_validator_datum(datum, appendix) + .ok_or("Can not parse appendix".to_string()), _ => Err(format!("Unknown version: {version}")), } } } -/// Converts [CandidateRegistration] domain type to [RegisterValidatorDatum::V0] encoded as [PlutusData]. +/// Converts [CandidateRegistration] domain type to [RegisterValidatorDatum::V1] encoded as [PlutusData]. pub fn candidate_registration_to_plutus_data( candidate_registration: &CandidateRegistration, ) -> PlutusData { - RegisterValidatorDatum::V0 { + RegisterValidatorDatum::V1 { stake_ownership: candidate_registration.stake_ownership.clone(), sidechain_pub_key: candidate_registration.partner_chain_pub_key.clone(), sidechain_signature: candidate_registration.partner_chain_signature.clone(), registration_utxo: candidate_registration.registration_utxo, own_pkh: candidate_registration.own_pkh, - aura_pub_key: candidate_registration.aura_pub_key.clone(), - grandpa_pub_key: candidate_registration.grandpa_pub_key.clone(), + session_keys: candidate_registration.session_keys.clone(), } .into() } @@ -113,14 +131,31 @@ impl From for CandidateRegistration { own_pkh, aura_pub_key, grandpa_pub_key, + } => { + let session_keys = vec![(*b"aura", aura_pub_key.0), (*b"gran", grandpa_pub_key.0)]; + CandidateRegistration { + stake_ownership, + partner_chain_pub_key: sidechain_pub_key, + partner_chain_signature: sidechain_signature, + registration_utxo, + own_pkh, + session_keys, + } + }, + RegisterValidatorDatum::V1 { + stake_ownership, + sidechain_pub_key, + sidechain_signature, + registration_utxo, + own_pkh, + session_keys, } => CandidateRegistration { stake_ownership, partner_chain_pub_key: sidechain_pub_key, partner_chain_signature: sidechain_signature, registration_utxo, own_pkh, - aura_pub_key, - grandpa_pub_key, + session_keys, }, } } @@ -154,6 +189,45 @@ fn decode_v0_register_validator_datum( }) } +fn decode_v1_session_keys(datum: &PlutusData) -> Option<([u8; 4], Vec)> { + let datum = datum.as_list()?; + + Some((datum.get(0).as_bytes().unwrap().try_into().ok()?, datum.get(1).as_bytes()?)) +} + +fn decode_v1_register_validator_datum( + datum: &PlutusData, + appendix: &PlutusData, +) -> Option { + let fields = appendix + .as_constr_plutus_data() + .filter(|datum| datum.alternative().is_zero()) + .filter(|datum| datum.data().len() == 5)? + .data(); + + let stake_ownership = decode_ada_based_staking_datum(fields.get(0))?; + let sidechain_pub_key = fields.get(1).as_bytes().map(SidechainPublicKey)?; + let sidechain_signature = fields.get(2).as_bytes().map(SidechainSignature)?; + let registration_utxo = decode_utxo_id_datum(fields.get(3))?; + + let session_keys = fields.get(4).as_list().and_then(|list_datums| { + list_datums + .into_iter() + .map(decode_v1_session_keys) + .collect::)>>>() + })?; + + let own_pkh = MainchainKeyHash(datum.as_bytes()?.try_into().ok()?); + Some(RegisterValidatorDatum::V1 { + stake_ownership, + sidechain_pub_key, + sidechain_signature, + registration_utxo, + own_pkh, + session_keys, + }) +} + /// Parses plutus data schema that was used before datum versioning was added. Kept for backwards compatibility. fn decode_legacy_register_validator_datum(datum: &PlutusData) -> Option { let fields = datum @@ -237,6 +311,35 @@ impl From for PlutusData { } .into() }, + RegisterValidatorDatum::V1 { + stake_ownership, + sidechain_pub_key, + sidechain_signature, + registration_utxo, + own_pkh, + session_keys, + } => { + let mut appendix_fields = PlutusList::new(); + appendix_fields.add(&stake_ownership_to_plutus_data(stake_ownership)); + appendix_fields.add(&PlutusData::new_bytes(sidechain_pub_key.0)); + appendix_fields.add(&PlutusData::new_bytes(sidechain_signature.0)); + appendix_fields.add(&utxo_id_to_plutus_data(registration_utxo)); + let mut plutus_session_keys = PlutusList::new(); + for (key_type, key) in session_keys { + let mut key_type_with_key = PlutusList::new(); + key_type_with_key.add(&PlutusData::new_bytes(key_type.to_vec())); + key_type_with_key.add(&PlutusData::new_bytes(key.to_vec())); + plutus_session_keys.add(&PlutusData::new_list(&key_type_with_key)); + } + appendix_fields.add(&PlutusData::new_list(&plutus_session_keys)); + let appendix = ConstrPlutusData::new(&BigNum::zero(), &appendix_fields); + VersionedGenericDatum { + datum: PlutusData::new_bytes(own_pkh.0.to_vec()), + appendix: PlutusData::new_constr_plutus_data(&appendix), + version: 1, + } + .into() + }, } } } @@ -286,6 +389,39 @@ mod tests { } } + fn test_datum_v1() -> RegisterValidatorDatum { + RegisterValidatorDatum::V1 { + stake_ownership: AdaBasedStaking { + pub_key: StakePoolPublicKey(hex!("bfbee74ab533f40979101057f96de62e95233f2a5216eb16b54106f09fd7350d")), + signature: MainchainSignature(hex!("28d1c3b7df297a60d24a3f88bc53d7029a8af35e8dd876764fd9e7a24203a3482a98263cc8ba2ddc7dc8e7faea31c2e7bad1f00e28c43bc863503e3172dc6b0a").into()), + }, + sidechain_pub_key: SidechainPublicKey(hex!("02fe8d1eb1bcb3432b1db5833ff5f2226d9cb5e65cee430558c18ed3a3c86ce1af").into()), + sidechain_signature: SidechainSignature(hex!("f8ec6c7f935d387aaa1693b3bf338cbb8f53013da8a5a234f9c488bacac01af259297e69aee0df27f553c0a1164df827d016125c16af93c99be2c19f36d2f66e").into()), + registration_utxo: UtxoId { + tx_hash: McTxHash(hex!("cdefe62b0a0016c2ccf8124d7dda71f6865283667850cc7b471f761d2bc1eb13")), + index: UtxoIndex(1), + }, + own_pkh: MainchainKeyHash(hex!("aabbccddeeff00aabbccddeeff00aabbccddeeff00aabbccddeeff00")), + session_keys: vec![ + ( + *b"crch", + hex!("cb6df9de1efca7a3998a8ead4e02159d5fa99c3e0d4fd6432667390bb4726854") + .into(), + ), + ( + *b"aura", + hex!("d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d") + .into(), + ), + ( + *b"gran", + hex!("88dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee") + .into(), + ), + ] + } + } + #[test] fn valid_legacy_registration() { let plutus_data = test_plutus_data!({