From 1377728028eaf4bb067dfdb930de7e02d4428d78 Mon Sep 17 00:00:00 2001 From: nanocryk <6422796+nanocryk@users.noreply.github.com> Date: Tue, 16 Jul 2024 17:17:58 +0200 Subject: [PATCH] More changes for Tanssi Data Preservers container chain spawning on assignment (#26) * add new interface fn + move rpc interface to tanssi * remove unused fn in Tanssi * fmt * remove generic --- Cargo.lock | 278 ++---------- Cargo.toml | 3 +- .../orchestrator-chain-interface/Cargo.toml | 1 + .../orchestrator-chain-interface/src/lib.rs | 49 +- .../Cargo.toml | 38 -- .../src/lib.rs | 315 ------------- .../src/ws_client.rs | 425 ------------------ .../authorities-noting-inherent/src/tests.rs | 22 +- .../container-chain-genesis-data/Cargo.toml | 54 +++ .../container-chain-genesis-data/src/json.rs | 286 ++++++++++++ .../container-chain-genesis-data/src/lib.rs | 163 +++++++ 11 files changed, 584 insertions(+), 1050 deletions(-) delete mode 100644 client/orchestrator-chain-rpc-interface/Cargo.toml delete mode 100644 client/orchestrator-chain-rpc-interface/src/lib.rs delete mode 100644 client/orchestrator-chain-rpc-interface/src/ws_client.rs create mode 100644 primitives/container-chain-genesis-data/Cargo.toml create mode 100644 primitives/container-chain-genesis-data/src/json.rs create mode 100644 primitives/container-chain-genesis-data/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 978d477..7fedc72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -586,54 +586,25 @@ dependencies = [ "futures-core", ] -[[package]] -name = "async-io" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" -dependencies = [ - "async-lock 2.8.0", - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-lite 1.13.0", - "log", - "parking", - "polling 2.8.0", - "rustix 0.37.27", - "slab", - "socket2 0.4.10", - "waker-fn", -] - [[package]] name = "async-io" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f97ab0c5b00a7cdbe5a371b9a782ee7be1316095885c8a4ea1daf490eb0ef65" dependencies = [ - "async-lock 3.3.0", + "async-lock", "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.2.0", + "futures-lite", "parking", - "polling 3.3.2", + "polling", "rustix 0.38.30", "slab", "tracing", "windows-sys 0.52.0", ] -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] - [[package]] name = "async-lock" version = "3.3.0" @@ -737,12 +708,6 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" -[[package]] -name = "base64" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" - [[package]] name = "base64ct" version = "1.6.0" @@ -1891,6 +1856,7 @@ version = "0.1.0" dependencies = [ "async-trait", "cumulus-primitives-core", + "dp-container-chain-genesis-data", "dp-core", "futures", "jsonrpsee", @@ -1903,36 +1869,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "dc-orchestrator-chain-rpc-interface" -version = "0.1.0" -dependencies = [ - "async-io 1.13.0", - "async-trait", - "dc-orchestrator-chain-interface", - "dp-core", - "futures", - "jsonrpsee", - "parity-scale-codec", - "polkadot-overseer", - "sc-client-api", - "sc-rpc-api", - "sc-service", - "schnellru", - "serde", - "serde_json", - "sp-api", - "sp-blockchain", - "sp-core", - "sp-state-machine", - "sp-storage 19.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.11.0)", - "thiserror", - "tokio", - "tokio-stream", - "tracing", - "url", -] - [[package]] name = "der" version = "0.7.8" @@ -2198,6 +2134,27 @@ dependencies = [ "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.11.0)", ] +[[package]] +name = "dp-container-chain-genesis-data" +version = "0.1.0" +dependencies = [ + "cumulus-primitives-core", + "frame-support", + "hex", + "hex-literal 0.3.4", + "log", + "parity-scale-codec", + "polkadot-primitives", + "scale-info", + "serde", + "serde_json", + "sp-core", + "sp-runtime", + "sp-state-machine", + "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.11.0)", + "sp-trie", +] + [[package]] name = "dp-core" version = "0.1.0" @@ -2545,15 +2502,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - [[package]] name = "fastrand" version = "2.0.1" @@ -3025,21 +2973,6 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" -[[package]] -name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite 0.2.13", - "waker-fn", -] - [[package]] name = "futures-lite" version = "2.2.0" @@ -3490,9 +3423,9 @@ dependencies = [ "hyper", "log", "rustls 0.21.10", - "rustls-native-certs 0.6.3", + "rustls-native-certs", "tokio", - "tokio-rustls 0.24.1", + "tokio-rustls", ] [[package]] @@ -3565,7 +3498,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6b0422c86d7ce0e97169cc42e04ae643caf278874a7a3c87b8150a220dc7e1e" dependencies = [ - "async-io 2.3.1", + "async-io", "core-foundation", "fnv", "futures", @@ -3784,30 +3717,8 @@ dependencies = [ "jsonrpsee-proc-macros", "jsonrpsee-server", "jsonrpsee-types", - "jsonrpsee-ws-client", - "tokio", - "tracing", -] - -[[package]] -name = "jsonrpsee-client-transport" -version = "0.22.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4978087a58c3ab02efc5b07c5e5e2803024536106fd5506f558db172c889b3aa" -dependencies = [ - "futures-util", - "http", - "jsonrpsee-core", - "pin-project", - "rustls-native-certs 0.7.0", - "rustls-pki-types", - "soketto", - "thiserror", "tokio", - "tokio-rustls 0.25.0", - "tokio-util", "tracing", - "url", ] [[package]] @@ -3819,19 +3730,16 @@ dependencies = [ "anyhow", "async-trait", "beef", - "futures-timer", "futures-util", "hyper", "jsonrpsee-types", "parking_lot 0.12.1", - "pin-project", "rand 0.8.5", "rustc-hash", "serde", "serde_json", "thiserror", "tokio", - "tokio-stream", "tracing", ] @@ -3885,19 +3793,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "jsonrpsee-ws-client" -version = "0.22.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58b9db2dfd5bb1194b0ce921504df9ceae210a345bc2f6c5a61432089bbab070" -dependencies = [ - "http", - "jsonrpsee-client-transport", - "jsonrpsee-core", - "jsonrpsee-types", - "url", -] - [[package]] name = "k256" version = "0.13.3" @@ -4511,12 +4406,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -6529,22 +6418,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26e85d3456948e650dff0cfc85603915847faf893ed1e66b020bb82ef4557120" -[[package]] -name = "polling" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite 0.2.13", - "windows-sys 0.48.0", -] - [[package]] name = "polling" version = "3.3.2" @@ -7423,20 +7296,6 @@ dependencies = [ "windows-sys 0.45.0", ] -[[package]] -name = "rustix" -version = "0.37.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - [[package]] name = "rustix" version = "0.38.30" @@ -7470,24 +7329,10 @@ checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", "ring 0.17.7", - "rustls-webpki 0.101.7", + "rustls-webpki", "sct", ] -[[package]] -name = "rustls" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" -dependencies = [ - "log", - "ring 0.17.7", - "rustls-pki-types", - "rustls-webpki 0.102.3", - "subtle 2.5.0", - "zeroize", -] - [[package]] name = "rustls-native-certs" version = "0.6.3" @@ -7495,20 +7340,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ "openssl-probe", - "rustls-pemfile 1.0.4", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-native-certs" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" -dependencies = [ - "openssl-probe", - "rustls-pemfile 2.1.2", - "rustls-pki-types", + "rustls-pemfile", "schannel", "security-framework", ] @@ -7522,22 +7354,6 @@ dependencies = [ "base64 0.21.7", ] -[[package]] -name = "rustls-pemfile" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" -dependencies = [ - "base64 0.22.0", - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" - [[package]] name = "rustls-webpki" version = "0.101.7" @@ -7548,17 +7364,6 @@ dependencies = [ "untrusted 0.9.0", ] -[[package]] -name = "rustls-webpki" -version = "0.102.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" -dependencies = [ - "ring 0.17.7", - "rustls-pki-types", - "untrusted 0.9.0", -] - [[package]] name = "rustversion" version = "1.0.14" @@ -10218,7 +10023,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" dependencies = [ "cfg-if", - "fastrand 2.0.1", + "fastrand", "redox_syscall 0.4.1", "rustix 0.38.30", "windows-sys 0.52.0", @@ -10429,17 +10234,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-rustls" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" -dependencies = [ - "rustls 0.22.4", - "rustls-pki-types", - "tokio", -] - [[package]] name = "tokio-stream" version = "0.1.15" @@ -10461,9 +10255,9 @@ dependencies = [ "futures-util", "log", "rustls 0.21.10", - "rustls-native-certs 0.6.3", + "rustls-native-certs", "tokio", - "tokio-rustls 0.24.1", + "tokio-rustls", "tungstenite", ] @@ -11044,12 +10838,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "waker-fn" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" - [[package]] name = "walkdir" version = "2.4.0" diff --git a/Cargo.toml b/Cargo.toml index 2704688..80add16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,6 @@ [workspace] members = [ "client/orchestrator-chain-interface", - "client/orchestrator-chain-rpc-interface", "container-chain-pallets/*", "container-chain-primitives/*", "pallets/*", @@ -21,10 +20,10 @@ ccp-xcm = { path = "container-chain-primitives/xcm", default-features = false } pallet-cc-authorities-noting = { path = "container-chain-pallets/authorities-noting", default-features = false } dc-orchestrator-chain-interface = { path = "client/orchestrator-chain-interface" } -dc-orchestrator-chain-rpc-interface = { path = "client/orchestrator-chain-rpc-interface" } dp-chain-state-snapshot = { path = "primitives/chain-state-snapshot", default-features = false } dp-collator-assignment = { path = "primitives/collator-assignment", default-features = false } dp-consensus = { path = "primitives/consensus", default-features = false } +dp-container-chain-genesis-data = { path = "primitives/container-chain-genesis-data", default-features = false } dp-core = { path = "primitives/core", default-features = false } dp-impl-tanssi-pallets-config = { path = "primitives/core", default-features = false } test-relay-sproof-builder = { path = "test-sproof-builder", default-features = false } diff --git a/client/orchestrator-chain-interface/Cargo.toml b/client/orchestrator-chain-interface/Cargo.toml index 3ec12aa..65ef373 100644 --- a/client/orchestrator-chain-interface/Cargo.toml +++ b/client/orchestrator-chain-interface/Cargo.toml @@ -13,6 +13,7 @@ parity-scale-codec = { workspace = true } thiserror = { workspace = true } # Dancekit +dp-container-chain-genesis-data = { workspace = true } dp-core = { workspace = true } # Substrate diff --git a/client/orchestrator-chain-interface/src/lib.rs b/client/orchestrator-chain-interface/src/lib.rs index 2b1eb0e..e8982b5 100644 --- a/client/orchestrator-chain-interface/src/lib.rs +++ b/client/orchestrator-chain-interface/src/lib.rs @@ -30,7 +30,8 @@ use { }; pub use { cumulus_primitives_core::relay_chain::Slot, - dp_core::{Hash as PHash, Header as PHeader}, + dp_container_chain_genesis_data::ContainerChainGenesisData, + dp_core::{BlockNumber, Hash as PHash, Header as PHeader}, }; #[derive(thiserror::Error, Debug)] @@ -56,6 +57,9 @@ pub enum OrchestratorChainError { #[error("Scale codec deserialization error: {0}")] DeserializationError(#[from] parity_scale_codec::Error), + #[error("API error: {0}")] + ApiError(#[from] sp_api::ApiError), + #[error("Unspecified error occured: {0}")] GenericError(String), } @@ -90,8 +94,6 @@ pub type OrchestratorChainResult = Result; /// Trait that provides all necessary methods for interaction between collator and orchestrator chain. #[async_trait::async_trait] pub trait OrchestratorChainInterface: Send + Sync { - type AuthorityId; - /// Fetch a storage item by key. async fn get_storage_by_key( &self, @@ -124,20 +126,23 @@ pub trait OrchestratorChainInterface: Send + Sync { &self, ) -> OrchestratorChainResult + Send>>>; - /// Return the set of authorities assigned to the paraId where - /// the first eligible key from the keystore is collating - async fn authorities( + async fn genesis_data( &self, orchestrator_parent: PHash, para_id: ParaId, - ) -> OrchestratorChainResult>>; + ) -> OrchestratorChainResult>; - /// Returns the minimum slot frequency for this para id. - async fn min_slot_freq( + async fn boot_nodes( &self, orchestrator_parent: PHash, para_id: ParaId, - ) -> OrchestratorChainResult>; + ) -> OrchestratorChainResult>>; + + async fn latest_block_number( + &self, + orchestrator_parent: PHash, + para_id: ParaId, + ) -> OrchestratorChainResult>; } #[async_trait::async_trait] @@ -145,8 +150,6 @@ impl OrchestratorChainInterface for Arc where T: OrchestratorChainInterface + ?Sized, { - type AuthorityId = T::AuthorityId; - fn overseer_handle(&self) -> OrchestratorChainResult { (**self).overseer_handle() } @@ -187,19 +190,29 @@ where (**self).finality_notification_stream().await } - async fn authorities( + async fn genesis_data( &self, orchestrator_parent: PHash, para_id: ParaId, - ) -> OrchestratorChainResult>> { - (**self).authorities(orchestrator_parent, para_id).await + ) -> OrchestratorChainResult> { + (**self).genesis_data(orchestrator_parent, para_id).await } - async fn min_slot_freq( + async fn boot_nodes( &self, orchestrator_parent: PHash, para_id: ParaId, - ) -> OrchestratorChainResult> { - (**self).min_slot_freq(orchestrator_parent, para_id).await + ) -> OrchestratorChainResult>> { + (**self).boot_nodes(orchestrator_parent, para_id).await + } + + async fn latest_block_number( + &self, + orchestrator_parent: PHash, + para_id: ParaId, + ) -> OrchestratorChainResult> { + (**self) + .latest_block_number(orchestrator_parent, para_id) + .await } } diff --git a/client/orchestrator-chain-rpc-interface/Cargo.toml b/client/orchestrator-chain-rpc-interface/Cargo.toml deleted file mode 100644 index 04a7069..0000000 --- a/client/orchestrator-chain-rpc-interface/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "dc-orchestrator-chain-rpc-interface" -authors = { workspace = true } -edition = "2021" -license = "GPL-3.0-only" -version = "0.1.0" - -[dependencies] -async-io = { workspace = true } -async-trait = { workspace = true } -futures = { workspace = true } -jsonrpsee = { workspace = true, features = [ "ws-client" ] } -schnellru = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true, features = [ "sync" ] } -tokio-stream = { workspace = true } -tracing = { workspace = true } -url = { workspace = true } - -# Dancekit -dc-orchestrator-chain-interface = { workspace = true } -dp-core = { workspace = true } - -# Substrate -parity-scale-codec = { workspace = true } -sc-client-api = { workspace = true } -sc-rpc-api = { workspace = true } -sc-service = { workspace = true } -sp-api = { workspace = true, features = [ "std" ] } -sp-blockchain = { workspace = true } -sp-core = { workspace = true } -sp-state-machine = { workspace = true, features = [ "std" ] } -sp-storage = { workspace = true } - -# Polkadot -polkadot-overseer = { workspace = true } diff --git a/client/orchestrator-chain-rpc-interface/src/lib.rs b/client/orchestrator-chain-rpc-interface/src/lib.rs deleted file mode 100644 index e7e110c..0000000 --- a/client/orchestrator-chain-rpc-interface/src/lib.rs +++ /dev/null @@ -1,315 +0,0 @@ -// Copyright (C) Moondance Labs Ltd. -// This file is part of Tanssi. - -// Tanssi is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Tanssi is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Tanssi. If not, see . - -mod ws_client; - -use { - async_trait::async_trait, - core::{marker::PhantomData, pin::Pin}, - dc_orchestrator_chain_interface::{ - OrchestratorChainError, OrchestratorChainInterface, OrchestratorChainResult, PHash, - PHeader, Slot, - }, - dp_core::ParaId, - futures::{Stream, StreamExt}, - jsonrpsee::{core::params::ArrayParams, rpc_params}, - sc_client_api::{StorageData, StorageProof}, - sc_rpc_api::state::ReadProof, - sc_service::TaskManager, - serde::de::DeserializeOwned, - sp_core::{Decode, Encode}, - sp_state_machine::StorageValue, - sp_storage::StorageKey, - tokio::sync::{mpsc, oneshot}, - url::Url, - ws_client::{JsonRpcRequest, WsClientRequest}, -}; - -const LOG_TARGET: &str = "orchestrator-rpc-client"; -const NOTIFICATION_CHANNEL_SIZE_LIMIT: usize = 20; - -/// Format url and force addition of a port -fn url_to_string_with_port(url: Url) -> Option { - // This is already validated on CLI side, just defensive here - if (url.scheme() != "ws" && url.scheme() != "wss") || url.host_str().is_none() { - tracing::warn!(target: LOG_TARGET, ?url, "Non-WebSocket URL or missing host."); - return None; - } - - // Either we have a user-supplied port or use the default for 'ws' or 'wss' here - Some(format!( - "{}://{}:{}{}{}", - url.scheme(), - url.host_str()?, - url.port_or_known_default()?, - url.path(), - url.query() - .map(|query| format!("?{}", query)) - .unwrap_or_default() - )) -} - -pub async fn create_client_and_start_worker( - urls: Vec, - task_manager: &mut TaskManager, - overseer_handle: Option, -) -> OrchestratorChainResult> { - let urls: Vec<_> = urls - .into_iter() - .filter_map(url_to_string_with_port) - .collect(); - let (worker, request_sender) = ws_client::ReconnectingWsClientWorker::new(urls) - .await - .map_err(|_| { - OrchestratorChainError::GenericError( - "Failed to connect to all provided Orchestrator chain RPC endpoints".to_string(), - ) - })?; - - task_manager - .spawn_essential_handle() - .spawn("orchestrator-rpc-worker", None, worker.run()); - - let client = OrchestratorChainRpcClient { - request_sender, - overseer_handle, - _phantom: PhantomData, - }; - - Ok(client) -} - -#[derive(Clone)] -pub struct OrchestratorChainRpcClient { - request_sender: mpsc::Sender, - overseer_handle: Option, - _phantom: PhantomData, -} - -impl OrchestratorChainRpcClient { - /// Call a call to `state_call` rpc method. - pub async fn call_remote_runtime_function( - &self, - method_name: &str, - hash: PHash, - payload: Option, - ) -> OrchestratorChainResult { - let payload_bytes = - payload.map_or(sp_core::Bytes(Vec::new()), |v| sp_core::Bytes(v.encode())); - let params = rpc_params! { - method_name, - payload_bytes, - hash - }; - let res = self - .request_tracing::("state_call", params, |err| { - tracing::trace!( - target: LOG_TARGET, - %method_name, - %hash, - error = %err, - "Error during call to 'state_call'.", - ); - }) - .await?; - Decode::decode(&mut &*res.0).map_err(Into::into) - } - - async fn request<'a, R>( - &self, - method: &'a str, - params: ArrayParams, - ) -> OrchestratorChainResult - where - R: DeserializeOwned + std::fmt::Debug, - { - self.request_tracing( - method, - params, - |e| tracing::trace!(target:LOG_TARGET, error = %e, %method, "Unable to complete RPC request"), - ).await - } - - fn send_register_message( - &self, - message_builder: impl FnOnce(mpsc::Sender) -> WsClientRequest, - ) -> OrchestratorChainResult> { - let (tx, rx) = mpsc::channel(NOTIFICATION_CHANNEL_SIZE_LIMIT); - self.request_sender - .try_send(message_builder(tx)) - .map_err(|e| OrchestratorChainError::WorkerCommunicationError(e.to_string()))?; - Ok(rx) - } - - /// Send a request to the RPC worker and awaits for a response. The worker is responsible - /// for retrying requests if connection dies. - async fn request_tracing<'a, R, OR>( - &self, - method: &'a str, - params: ArrayParams, - trace_error: OR, - ) -> OrchestratorChainResult - where - R: DeserializeOwned + std::fmt::Debug, - OR: Fn(&OrchestratorChainError), - { - let (response_sender, response_receiver) = oneshot::channel(); - - let request = WsClientRequest::JsonRpcRequest(JsonRpcRequest { - method: method.into(), - params, - response_sender, - }); - self.request_sender.send(request).await.map_err(|err| { - OrchestratorChainError::WorkerCommunicationError(format!( - "Unable to send message to RPC worker: {}", - err - )) - })?; - - let response = response_receiver.await.map_err(|err| { - OrchestratorChainError::WorkerCommunicationError(format!( - "RPC worker channel closed. This can hint and connectivity issues with the supplied RPC endpoints. Message: {}", - err - )) - })??; - - serde_json::from_value(response).map_err(|_| { - trace_error(&OrchestratorChainError::GenericError( - "Unable to deserialize value".to_string(), - )); - OrchestratorChainError::RpcCallError( - method.to_string(), - "failed to decode returned value".to_string(), - ) - }) - } - - /// Retrieve storage item at `storage_key` - pub async fn state_get_storage( - &self, - storage_key: StorageKey, - at: Option, - ) -> OrchestratorChainResult> { - let params = rpc_params![storage_key, at]; - self.request("state_getStorage", params).await - } - - /// Get read proof for `storage_keys` - pub async fn state_get_read_proof( - &self, - storage_keys: Vec, - at: Option, - ) -> OrchestratorChainResult> { - let params = rpc_params![storage_keys, at]; - self.request("state_getReadProof", params).await - } -} - -#[async_trait] -impl OrchestratorChainInterface for OrchestratorChainRpcClient { - type AuthorityId = T; - - /// Fetch a storage item by key. - async fn get_storage_by_key( - &self, - orchestrator_parent: PHash, - key: &[u8], - ) -> OrchestratorChainResult> { - let storage_key = StorageKey(key.to_vec()); - self.state_get_storage(storage_key, Some(orchestrator_parent)) - .await - .map(|storage_data| storage_data.map(|sv| sv.0)) - } - - /// Get a handle to the overseer. - fn overseer_handle(&self) -> OrchestratorChainResult { - self.overseer_handle - .clone() - .ok_or(OrchestratorChainError::GenericError( - "OrchestratorChainRpcClient doesn't contain an Overseer Handle".to_string(), - )) - } - - /// Generate a storage read proof. - async fn prove_read( - &self, - orchestrator_parent: PHash, - relevant_keys: &[Vec], - ) -> OrchestratorChainResult { - let mut cloned = Vec::new(); - cloned.extend_from_slice(relevant_keys); - let storage_keys: Vec = cloned.into_iter().map(StorageKey).collect(); - - self.state_get_read_proof(storage_keys, Some(orchestrator_parent)) - .await - .map(|read_proof| { - StorageProof::new(read_proof.proof.into_iter().map(|bytes| bytes.to_vec())) - }) - } - - /// Get a stream of import block notifications. - async fn import_notification_stream( - &self, - ) -> OrchestratorChainResult + Send>>> { - let rx = self.send_register_message(WsClientRequest::RegisterImportListener)?; - let stream = tokio_stream::wrappers::ReceiverStream::new(rx); - Ok(stream.boxed()) - } - - /// Get a stream of new best block notifications. - async fn new_best_notification_stream( - &self, - ) -> OrchestratorChainResult + Send>>> { - let rx = self.send_register_message(WsClientRequest::RegisterBestHeadListener)?; - let stream = tokio_stream::wrappers::ReceiverStream::new(rx); - Ok(stream.boxed()) - } - - /// Get a stream of finality notifications. - async fn finality_notification_stream( - &self, - ) -> OrchestratorChainResult + Send>>> { - let rx = self.send_register_message(WsClientRequest::RegisterFinalizationListener)?; - let stream = tokio_stream::wrappers::ReceiverStream::new(rx); - Ok(stream.boxed()) - } - - /// Return the set of authorities assigned to the paraId where - /// the first eligible key from the keystore is collating - async fn authorities( - &self, - orchestrator_parent: PHash, - para_id: ParaId, - ) -> OrchestratorChainResult>> { - self.call_remote_runtime_function("para_id_authorities", orchestrator_parent, Some(para_id)) - .await - } - - /// Returns the minimum slot frequency for this para id. - async fn min_slot_freq( - &self, - orchestrator_parent: PHash, - para_id: ParaId, - ) -> OrchestratorChainResult> { - self.call_remote_runtime_function( - "parathread_slot_frequency", - orchestrator_parent, - Some(para_id), - ) - .await - } -} diff --git a/client/orchestrator-chain-rpc-interface/src/ws_client.rs b/client/orchestrator-chain-rpc-interface/src/ws_client.rs deleted file mode 100644 index afcc324..0000000 --- a/client/orchestrator-chain-rpc-interface/src/ws_client.rs +++ /dev/null @@ -1,425 +0,0 @@ -// Copyright (C) Moondance Labs Ltd. -// This file is part of Tanssi. - -// Tanssi is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Tanssi is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Tanssi. If not, see . - -use { - futures::{ - future::BoxFuture, - stream::{FuturesUnordered, StreamExt}, - FutureExt, - }, - jsonrpsee::{ - core::{ - client::{Client as JsonRpcClient, ClientT as _, Subscription}, - params::ArrayParams, - ClientError as JsonRpseeError, JsonValue, - }, - ws_client::WsClientBuilder, - }, - sc_rpc_api::chain::ChainApiClient, - schnellru::{ByLength, LruMap}, - std::sync::Arc, - tokio::sync::{mpsc, oneshot}, -}; - -const LOG_TARGET: &str = "reconnecting-websocket-client-orchestrator"; - -type RpcRequestFuture = BoxFuture<'static, Result<(), JsonRpcRequest>>; - -/// A Json Rpc/Rpsee request with a oneshot sender to send the request's response. -pub struct JsonRpcRequest { - pub method: String, - pub params: ArrayParams, - pub response_sender: oneshot::Sender>, -} - -pub enum WsClientRequest { - JsonRpcRequest(JsonRpcRequest), - RegisterBestHeadListener(mpsc::Sender), - RegisterImportListener(mpsc::Sender), - RegisterFinalizationListener(mpsc::Sender), -} - -enum ConnectionStatus { - Connected, - Disconnected { - failed_request: Option, - }, -} - -/// Worker that manage a WebSocket connection and handle disconnects by changing endpoint and -/// retrying pending requests. -/// -/// Is first created with [`ReconnectingWsClientWorker::new`], which returns both a -/// [`ReconnectingWsClientWorker`] and an [`mpsc::Sender`] to send the requests. -/// [`ReconnectingWsClientWorker::run`] must the be called and the returned future queued in -/// a tokio executor. -pub struct ReconnectingWsClientWorker { - urls: Vec, - active_client: Arc, - active_index: usize, - - request_receiver: mpsc::Receiver, - - imported_header_listeners: Vec>, - finalized_header_listeners: Vec>, - best_header_listeners: Vec>, -} - -struct OrchestratorSubscription { - import_subscription: Subscription, - finalized_subscription: Subscription, - best_subscription: Subscription, -} - -/// Connects to a ws server by cycle throught all provided urls from the starting position until -/// each one one was tried. Stops once a connection was succesfully made. -async fn connect_next_available_rpc_server( - urls: &[String], - starting_position: usize, -) -> Result<(usize, Arc), ()> { - tracing::debug!(target: LOG_TARGET, starting_position, "Connecting to RPC server."); - - for (counter, url) in urls - .iter() - .cycle() - .skip(starting_position) - .take(urls.len()) - .enumerate() - { - let index = (starting_position + counter) % urls.len(); - - tracing::info!( - target: LOG_TARGET, - index, - url, - "Trying to connect to next external orchestrator node.", - ); - - match WsClientBuilder::default().build(&url).await { - Ok(ws_client) => return Ok((index, Arc::new(ws_client))), - Err(err) => tracing::debug!(target: LOG_TARGET, url, ?err, "Unable to connect."), - }; - } - Err(()) -} - -impl ReconnectingWsClientWorker { - /// Create a new worker that will connect to the provided URLs. - pub async fn new(urls: Vec) -> Result<(Self, mpsc::Sender), ()> { - if urls.is_empty() { - return Err(()); - } - - let (active_index, active_client) = connect_next_available_rpc_server(&urls, 0).await?; - let (request_sender, request_receiver) = mpsc::channel(100); - - Ok(( - Self { - urls, - active_client, - active_index, - request_receiver, - best_header_listeners: vec![], - imported_header_listeners: vec![], - finalized_header_listeners: vec![], - }, - request_sender, - )) - } - - /// Change RPC server for future requests. - async fn connect_to_new_rpc_server(&mut self) -> Result<(), ()> { - let (active_index, active_client) = - connect_next_available_rpc_server(&self.urls, self.active_index + 1).await?; - self.active_index = active_index; - self.active_client = active_client; - Ok(()) - } - - /// Send the request to the current client. If this connection becomes dead, the returned future - /// will return the request so it can be sent to another client. - fn send_request( - &self, - JsonRpcRequest { - method, - params, - response_sender, - }: JsonRpcRequest, - ) -> RpcRequestFuture { - let client = self.active_client.clone(); - async move { - let response = client.request(&method, params.clone()).await; - - // We should only return the original request in case - // the websocket connection is dead and requires a restart. - // Other errors should be forwarded to the request caller. - if let Err(JsonRpseeError::RestartNeeded(_)) = response { - return Err(JsonRpcRequest { - method, - params, - response_sender, - }); - } - - if let Err(err) = response_sender.send(response) { - tracing::debug!( - target: LOG_TARGET, - ?err, - "Recipient no longer interested in request result" - ); - } - - Ok(()) - } - .boxed() - } - - async fn get_subscriptions(&self) -> Result { - let import_subscription = >::subscribe_all_heads(&self.active_client) - .await - .map_err(|e| { - tracing::error!( - target: LOG_TARGET, - ?e, - "Unable to open `chain_subscribeAllHeads` subscription." - ); - e - })?; - - let best_subscription = >::subscribe_new_heads(&self.active_client) - .await - .map_err(|e| { - tracing::error!( - target: LOG_TARGET, - ?e, - "Unable to open `chain_subscribeNewHeads` subscription." - ); - e - })?; - - let finalized_subscription = >::subscribe_finalized_heads(&self.active_client) - .await - .map_err(|e| { - tracing::error!( - target: LOG_TARGET, - ?e, - "Unable to open `chain_subscribeFinalizedHeads` subscription." - ); - e - })?; - - Ok(OrchestratorSubscription { - import_subscription, - best_subscription, - finalized_subscription, - }) - } - - /// Handle a reconnection by fnding a new RPC server and sending all pending requests. - async fn handle_reconnect( - &mut self, - pending_requests: &mut FuturesUnordered, - first_failed_request: Option, - ) -> Result<(), String> { - let mut requests_to_retry = Vec::new(); - if let Some(req) = first_failed_request { - requests_to_retry.push(req) - } - - // All pending requests will return an error since the websocket connection is dead. - // Draining the pending requests should be fast. - while !pending_requests.is_empty() { - if let Some(Err(req)) = pending_requests.next().await { - requests_to_retry.push(req); - } - } - - // Connect to new RPC server if possible. - if self.connect_to_new_rpc_server().await.is_err() { - return Err("Unable to find valid external RPC server, shutting down.".to_string()); - } - - // Retry requests. - for req in requests_to_retry.into_iter() { - pending_requests.push(self.send_request(req)); - } - - // Get subscriptions from new endpoint. - self.get_subscriptions().await.map_err(|e| { - format!("Not able to create streams from newly connected RPC server, shutting down. err: {:?}", e) - })?; - - Ok(()) - } - - pub async fn run(mut self) { - let mut pending_requests = FuturesUnordered::new(); - let mut connection_status = ConnectionStatus::Connected; - - let Ok(mut subscriptions) = self.get_subscriptions().await else { - tracing::error!(target: LOG_TARGET, "Unable to fetch subscriptions on initial connection."); - return; - }; - - let mut imported_blocks_cache = LruMap::new(ByLength::new(40)); - let mut last_seen_finalized_num: dp_core::BlockNumber = 0; - - loop { - // Handle reconnection. - if let ConnectionStatus::Disconnected { failed_request } = connection_status { - if let Err(message) = self - .handle_reconnect(&mut pending_requests, failed_request) - .await - { - tracing::error!( - target: LOG_TARGET, - message, - "Unable to reconnect, stopping worker." - ); - return; - } - - connection_status = ConnectionStatus::Connected; - } - - tokio::select! { - // New request received. - req = self.request_receiver.recv() => match req { - Some(WsClientRequest::JsonRpcRequest(req)) => { - pending_requests.push(self.send_request(req)); - }, - Some(WsClientRequest::RegisterBestHeadListener(tx)) => { - self.best_header_listeners.push(tx); - }, - Some(WsClientRequest::RegisterImportListener(tx)) => { - self.imported_header_listeners.push(tx); - }, - Some(WsClientRequest::RegisterFinalizationListener(tx)) => { - self.finalized_header_listeners.push(tx); - }, - None => { - tracing::error!(target: LOG_TARGET, "RPC client receiver closed. Stopping RPC Worker."); - return; - } - }, - // We poll pending request futures. If one completes with an `Err`, it means the - // ws client was disconnected and we need to reconnect to a new ws client. - pending = pending_requests.next(), if !pending_requests.is_empty() => { - if let Some(Err(req)) = pending { - connection_status = ConnectionStatus::Disconnected { failed_request: Some(req) }; - } - }, - import_event = subscriptions.import_subscription.next() => { - match import_event { - Some(Ok(header)) => { - let hash = header.hash(); - if imported_blocks_cache.peek(&hash).is_some() { - tracing::debug!( - target: LOG_TARGET, - number = header.number, - ?hash, - "Duplicate imported block header. This might happen after switching to a new RPC node. Skipping distribution." - ); - continue; - } - imported_blocks_cache.insert(hash, ()); - distribute(header, &mut self.imported_header_listeners); - }, - None => { - tracing::error!(target: LOG_TARGET, "Subscription closed."); - connection_status = ConnectionStatus::Disconnected { failed_request: None}; - }, - Some(Err(error)) => { - tracing::error!(target: LOG_TARGET, ?error, "Error in RPC subscription."); - connection_status = ConnectionStatus::Disconnected { failed_request: None}; - }, - } - }, - best_header_event = subscriptions.best_subscription.next() => { - match best_header_event { - Some(Ok(header)) => distribute(header, &mut self.best_header_listeners), - None => { - tracing::error!(target: LOG_TARGET, "Subscription closed."); - connection_status = ConnectionStatus::Disconnected { failed_request: None}; - }, - Some(Err(error)) => { - tracing::error!(target: LOG_TARGET, ?error, "Error in RPC subscription."); - connection_status = ConnectionStatus::Disconnected { failed_request: None}; - }, - } - } - finalized_event = subscriptions.finalized_subscription.next() => { - match finalized_event { - Some(Ok(header)) if header.number > last_seen_finalized_num => { - last_seen_finalized_num = header.number; - distribute(header, &mut self.finalized_header_listeners); - }, - Some(Ok(header)) => { - tracing::debug!( - target: LOG_TARGET, - number = header.number, - last_seen_finalized_num, - "Duplicate finalized block header. This might happen after switching to a new RPC node. Skipping distribution." - ); - }, - None => { - tracing::error!(target: LOG_TARGET, "Subscription closed."); - connection_status = ConnectionStatus::Disconnected { failed_request: None}; - }, - Some(Err(error)) => { - tracing::error!(target: LOG_TARGET, ?error, "Error in RPC subscription."); - connection_status = ConnectionStatus::Disconnected { failed_request: None}; - }, - } - } - } - } - } -} - -/// Send `value` through all channels contained in `senders`. -/// If no one is listening to the sender, it is removed from the vector. -pub fn distribute(value: T, senders: &mut Vec>) { - senders.retain_mut(|e| { - match e.try_send(value.clone()) { - // Receiver has been dropped, remove Sender from list. - Err(mpsc::error::TrySendError::Closed(_)) => false, - // Channel is full. This should not happen. - // TODO: Improve error handling here - // https://github.com/paritytech/cumulus/issues/1482 - Err(error) => { - tracing::error!(target: LOG_TARGET, ?error, "Event distribution channel has reached its limit. This can lead to missed notifications."); - true - }, - _ => true, - } - }); -} diff --git a/container-chain-primitives/authorities-noting-inherent/src/tests.rs b/container-chain-primitives/authorities-noting-inherent/src/tests.rs index 1135cb1..7e3bc20 100644 --- a/container-chain-primitives/authorities-noting-inherent/src/tests.rs +++ b/container-chain-primitives/authorities-noting-inherent/src/tests.rs @@ -26,7 +26,9 @@ use { InboundDownwardMessage, InboundHrmpMessage, ParaId, PersistedValidationData, }, cumulus_relay_chain_interface::{PHash, PHeader, RelayChainInterface, RelayChainResult}, - dc_orchestrator_chain_interface::{OrchestratorChainInterface, OrchestratorChainResult, Slot}, + dc_orchestrator_chain_interface::{ + BlockNumber, ContainerChainGenesisData, OrchestratorChainInterface, OrchestratorChainResult, + }, dp_core::{well_known_keys, Header as OrchestratorHeader}, futures::Stream, polkadot_overseer::Handle, @@ -84,8 +86,6 @@ impl DummyRelayChainInterface { #[async_trait] impl OrchestratorChainInterface for DummyOrchestratorChainInterface { - type AuthorityId = (); - fn overseer_handle(&self) -> OrchestratorChainResult { unimplemented!("Not needed for test") } @@ -131,19 +131,27 @@ impl OrchestratorChainInterface for DummyOrchestratorChainInterface { unimplemented!("Not needed for test") } - async fn authorities( + async fn genesis_data( + &self, + _orchestrator_parent: PHash, + _para_id: ParaId, + ) -> OrchestratorChainResult> { + unimplemented!("Not needed for test") + } + + async fn boot_nodes( &self, _orchestrator_parent: PHash, _para_id: ParaId, - ) -> OrchestratorChainResult>> { + ) -> OrchestratorChainResult>> { unimplemented!("Not needed for test") } - async fn min_slot_freq( + async fn latest_block_number( &self, _orchestrator_parent: PHash, _para_id: ParaId, - ) -> OrchestratorChainResult> { + ) -> OrchestratorChainResult> { unimplemented!("Not needed for test") } } diff --git a/primitives/container-chain-genesis-data/Cargo.toml b/primitives/container-chain-genesis-data/Cargo.toml new file mode 100644 index 0000000..22f69d2 --- /dev/null +++ b/primitives/container-chain-genesis-data/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "dp-container-chain-genesis-data" +authors = { workspace = true } +description = "Primitives related to container-chain genesis data" +edition = "2021" +license = "GPL-3.0-only" +version = "0.1.0" + +[package.metadata.docs.rs] +targets = [ "x86_64-unknown-linux-gnu" ] + +[dependencies] +hex = { workspace = true, optional = true, features = [ "alloc" ] } +hex-literal = { workspace = true } + +frame-support = { workspace = true } +log = { workspace = true } +parity-scale-codec = { workspace = true } +scale-info = { workspace = true } +serde = { workspace = true, features = [ "derive" ] } +serde_json = { workspace = true, optional = true } +sp-core = { workspace = true } +sp-runtime = { workspace = true, optional = true } +sp-state-machine = { workspace = true, optional = true } +sp-std = { workspace = true } +sp-trie = { workspace = true, optional = true } + +# Cumulus +cumulus-primitives-core = { workspace = true, optional = true } + +# Polkadot +polkadot-primitives = { workspace = true, optional = true } + +[features] +default = [ "std" ] +std = [ + "cumulus-primitives-core/std", + "frame-support/std", + "hex?/std", + "log/std", + "parity-scale-codec/std", + "polkadot-primitives", + "polkadot-primitives?/std", + "scale-info/std", + "serde/std", + "serde/std", + "serde_json?/std", + "sp-core/std", + "sp-runtime/std", + "sp-state-machine/std", + "sp-std/std", + "sp-trie/std", +] +json = [ "hex", "serde_json" ] diff --git a/primitives/container-chain-genesis-data/src/json.rs b/primitives/container-chain-genesis-data/src/json.rs new file mode 100644 index 0000000..31edcd3 --- /dev/null +++ b/primitives/container-chain-genesis-data/src/json.rs @@ -0,0 +1,286 @@ +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. + +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see + +//! Helper functions to convert from `ContainerChainGenesisData` to JSON values and back + +use { + crate::{ContainerChainGenesisData, ContainerChainGenesisDataItem, Properties}, + cumulus_primitives_core::ParaId, +}; + +pub type ContainerChainGenesisDataResult = + Result<(ParaId, ContainerChainGenesisData, Vec>), String>; + +/// Reads a raw ChainSpec file stored in `path`, and returns its `ParaId` and +/// a `ContainerChainGenesisData` that can be used to recreate the ChainSpec later. +pub fn container_chain_genesis_data_from_path(path: &str) -> ContainerChainGenesisDataResult { + // Read raw chainspec file + let raw_chainspec_str = std::fs::read_to_string(path) + .map_err(|_e| format!("ChainSpec for container chain not found at {:?}", path))?; + + container_chain_genesis_data_from_str(&raw_chainspec_str) +} + +pub fn container_chain_genesis_data_from_str( + raw_chainspec_str: &str, +) -> ContainerChainGenesisDataResult { + let raw_chainspec_json: serde_json::Value = + serde_json::from_str(raw_chainspec_str).map_err(|e| e.to_string())?; + + container_chain_genesis_data_from_json(&raw_chainspec_json) +} + +pub fn container_chain_genesis_data_from_json( + raw_chainspec_json: &serde_json::Value, +) -> ContainerChainGenesisDataResult { + // TODO: we are manually parsing a json file here, maybe we can leverage the existing + // chainspec deserialization code. + // TODO: this bound checking may panic, but that shouldn't be too dangerous because this + // function is only used by the `build-spec` command. + let para_id: u32 = u32::try_from(raw_chainspec_json["para_id"].as_u64().unwrap()).unwrap(); + let name: String = raw_chainspec_json["name"].as_str().unwrap().to_owned(); + let id: String = raw_chainspec_json["id"].as_str().unwrap().to_owned(); + let fork_id: Option = raw_chainspec_json["fork_id"].as_str().map(|x| x.to_owned()); + let genesis_raw_top_json = &raw_chainspec_json["genesis"]["raw"]["top"]; + let storage = storage_from_chainspec_json(genesis_raw_top_json)?; + let properties_json = &raw_chainspec_json["properties"]; + let properties = properties_from_chainspec_json(properties_json); + let boot_nodes: Vec = + raw_chainspec_json["bootNodes"].as_array().unwrap().clone(); + let boot_nodes: Vec> = boot_nodes + .into_iter() + .map(|x| { + let bytes = x.as_str().unwrap().as_bytes(); + bytes.to_vec() + }) + .collect(); + + Ok(( + para_id.into(), + ContainerChainGenesisData { + storage, + name: name.into(), + id: id.into(), + fork_id: fork_id.map(|x| x.into()), + extensions: vec![], + properties, + }, + boot_nodes, + )) +} + +pub fn storage_from_chainspec_json( + genesis_raw_top_json: &serde_json::Value, +) -> Result, String> { + let genesis_data_map = genesis_raw_top_json + .as_object() + .ok_or("genesis.raw.top is not an object".to_string())?; + + let mut genesis_data_vec = Vec::with_capacity(genesis_data_map.len()); + + for (key, value) in genesis_data_map { + let key_hex = key + .strip_prefix("0x") + .ok_or("key does not start with 0x".to_string())?; + let value = value.as_str().ok_or("value is not a string".to_string())?; + let value_hex = value + .strip_prefix("0x") + .ok_or("value does not start with 0x".to_string())?; + + let key_bytes = hex::decode(key_hex).map_err(|e| e.to_string())?; + let value_bytes = hex::decode(value_hex).map_err(|e| e.to_string())?; + + genesis_data_vec.push((key_bytes, value_bytes).into()); + } + + // This sort is just to make the UI a bit easier to follow, + // sorting the storage is not a requirement. + // Maybe it is not even needed if the `genesis_data_map` iterator is ordered. + // Unstable sort is fine because this was created by iterating over a map, + // so it won't have two equal keys + genesis_data_vec.sort_unstable(); + + Ok(genesis_data_vec) +} + +/// Read `TokenMetadata` from a JSON value. The value is expected to be a map. +/// In case of error, the default `TokenMetadata` is returned. +pub fn properties_from_chainspec_json(properties_json: &serde_json::Value) -> Properties { + let mut properties: Properties = Properties::default(); + if let Some(x) = properties_json + .get("ss58Format") + .and_then(|x| u32::try_from(x.as_u64()?).ok()) + .or_else(|| { + log::warn!( + "Failed to read properties.ss58Format from container chain chain spec, using default value instead. Invalid value was: {:?}", + properties_json.get("ss58Format") + ); + + None + }) + { + properties.token_metadata.ss58_format = x; + } + if let Some(x) = properties_json + .get("tokenDecimals") + .and_then(|x: &serde_json::Value| u32::try_from(x.as_u64()?).ok()).or_else(|| { + log::warn!( + "Failed to read properties.tokenDecimals from container chain chain spec, using default value instead. Invalid value was: {:?}", + properties_json.get("tokenDecimals") + ); + + None + }) + { + properties.token_metadata.token_decimals = x; + } + if let Some(x) = properties_json.get("tokenSymbol").and_then(|x| { + let xs = x.as_str()?; + let xv: Vec = xs.to_string().into(); + + xv.try_into().ok() + }).or_else(|| { + log::warn!( + "Failed to read properties.tokenSymbol from container chain chain spec, using default value instead. Invalid value was: {:?}", + properties_json.get("tokenSymbol") + ); + + None + }) { + properties.token_metadata.token_symbol = x; + } + if let Some(x) = properties_json.get("isEthereum").and_then(|x| { + x.as_bool() + }).or_else(|| { + log::warn!( + "Failed to read properties.isEthereum from container chain chain spec, using default value instead. Invalid value was: {:?}", + properties_json.get("isEthereum") + ); + + None + }) { + properties.is_ethereum = x; + } + + properties +} + +pub fn properties_to_map( + properties: &Properties, +) -> Result, String> { + // TODO: we can just derive Serialize for genesis_data.properties instead of this hack, + // just ensure that the field names match. And "tokenSymbol" must be a string, in the struct + // it is defined as a Vec. + let properties = vec![ + ( + "ss58Format", + serde_json::Value::from(properties.token_metadata.ss58_format), + ), + ( + "tokenDecimals", + serde_json::Value::from(properties.token_metadata.token_decimals), + ), + ( + "tokenSymbol", + serde_json::Value::from( + String::from_utf8(properties.token_metadata.token_symbol.to_vec()) + .map_err(|e| format!("tokenSymbol is not valid UTF8: {}", e))?, + ), + ), + ( + "isEthereum", + serde_json::Value::from(properties.is_ethereum), + ), + ] + .into_iter() + .map(|(k, v)| (k.to_string(), v)) + .collect(); + + Ok(properties) +} + +#[cfg(test)] +mod tests { + use sp_core::ConstU32; + + use super::*; + + fn expected_container_chain_genesis_data() -> ContainerChainGenesisData { + let para_id = 2000; + + ContainerChainGenesisData { + storage: vec![(b"code".to_vec(), vec![1, 2, 3, 4, 5, 6]).into()], + name: format!("Container Chain {}", para_id).into(), + id: format!("container-chain-{}", para_id).into(), + fork_id: None, + extensions: vec![], + properties: Default::default(), + } + } + + fn expected_string() -> &'static str { + // TODO: this should be improved: + // * name should be a string "Container Chain 2000" + // * id should be a string + // * token_symbol should be a string + // * storage should be a map: + // "storage": { "0x636f6465": "0x010203040506" } + r#"{ + "storage": [ + { + "key": "0x636f6465", + "value": "0x010203040506" + } + ], + "name": "0x436f6e7461696e657220436861696e2032303030", + "id": "0x636f6e7461696e65722d636861696e2d32303030", + "fork_id": null, + "extensions": "0x", + "properties": { + "token_metadata": { + "token_symbol": [ + 85, + 78, + 73, + 84 + ], + "ss58_format": 42, + "token_decimals": 12 + }, + "is_ethereum": false + } + }"# + } + + #[test] + fn test_serde_serialize() { + let x = expected_container_chain_genesis_data(); + let xv = serde_json::to_value(x).unwrap(); + // Regenerate expected string using + //println!("{}", serde_json::to_string_pretty(&x).unwrap()); + let expected = expected_string(); + let ev: serde_json::Value = serde_json::from_str(expected).unwrap(); + assert_eq!(xv, ev); + } + + #[test] + fn test_serde_deserialize() { + let expected = expected_container_chain_genesis_data(); + let s = expected_string(); + let x: ContainerChainGenesisData = serde_json::from_str(s).unwrap(); + assert_eq!(x, expected); + } +} diff --git a/primitives/container-chain-genesis-data/src/lib.rs b/primitives/container-chain-genesis-data/src/lib.rs new file mode 100644 index 0000000..78c1f93 --- /dev/null +++ b/primitives/container-chain-genesis-data/src/lib.rs @@ -0,0 +1,163 @@ +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. + +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see + +//! Data structures used to store a ContainerChain ChainSpec in the registrar pallet + +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::{CloneNoBound, DebugNoBound, DefaultNoBound, EqNoBound, PartialEqNoBound}; + +use { + frame_support::BoundedVec, + parity_scale_codec::{Decode, Encode}, + sp_std::vec::Vec, +}; + +#[cfg(feature = "json")] +pub mod json; + +// TODO: improve serialization of storage field +// Currently it looks like this: +/* +"storage": [ + { + "key": "0x0d715f2646c8f85767b5d2764bb2782604a74d81251e398fd8a0a4d55023bb3f" + "value": "0xd1070000" + }, + { + "key": "0x0d715f2646c8f85767b5d2764bb278264e7b9012096b41c4eb3aaf947f6ea429" + "value": "0x0000" + } +] + */ +// Ideally it would be: +/* +"storage": { + "0x0d715f2646c8f85767b5d2764bb2782604a74d81251e398fd8a0a4d55023bb3f": "0xd1070000", + "0x0d715f2646c8f85767b5d2764bb278264e7b9012096b41c4eb3aaf947f6ea429": "0x0000" +} + */ +// This is just so it looks nicer on polkadot.js, the functionality is the same +// The original approach of using `storage: BTreeMap, Vec>` looks very bad +// in polkadot.js, because `Vec` is serialized as `[12, 51, 124]` instead of hex. +// That's why we use `serde(with = "sp_core::bytes")` everywhere, to convert it to hex. +#[derive( + DebugNoBound, + CloneNoBound, + EqNoBound, + DefaultNoBound, + PartialEqNoBound, + Encode, + Decode, + scale_info::TypeInfo, + serde::Deserialize, + serde::Serialize, +)] +#[serde(bound = "")] +pub struct ContainerChainGenesisData { + pub storage: Vec, + // TODO: make all these Vec bounded + #[serde(with = "sp_core::bytes")] + pub name: Vec, + #[serde(with = "sp_core::bytes")] + pub id: Vec, + pub fork_id: Option>, + #[serde(with = "sp_core::bytes")] + pub extensions: Vec, + pub properties: Properties, +} + +#[derive( + DebugNoBound, + CloneNoBound, + EqNoBound, + DefaultNoBound, + PartialEqNoBound, + Encode, + Decode, + scale_info::TypeInfo, + serde::Deserialize, + serde::Serialize, +)] +#[serde(bound = "")] +pub struct Properties { + pub token_metadata: TokenMetadata, + pub is_ethereum: bool, +} + +#[derive( + DebugNoBound, + CloneNoBound, + EqNoBound, + PartialEqNoBound, + Encode, + Decode, + scale_info::TypeInfo, + serde::Deserialize, + serde::Serialize, +)] +#[serde(bound = "")] +pub struct TokenMetadata { + pub token_symbol: BoundedVec>, + pub ss58_format: u32, + pub token_decimals: u32, +} + +impl Default for TokenMetadata { + fn default() -> Self { + // Default values from polkadot.js + Self { + token_symbol: BoundedVec::truncate_from(b"UNIT".to_vec()), + ss58_format: 42, + token_decimals: 12, + } + } +} + +#[derive( + Debug, + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + Encode, + Decode, + scale_info::TypeInfo, + serde::Deserialize, + serde::Serialize, +)] +pub struct ContainerChainGenesisDataItem { + #[serde(with = "sp_core::bytes")] + pub key: Vec, + #[serde(with = "sp_core::bytes")] + pub value: Vec, +} + +impl From<(Vec, Vec)> for ContainerChainGenesisDataItem { + fn from(x: (Vec, Vec)) -> Self { + Self { + key: x.0, + value: x.1, + } + } +} + +impl From for (Vec, Vec) { + fn from(x: ContainerChainGenesisDataItem) -> Self { + (x.key, x.value) + } +}