diff --git a/.config/hakari.toml b/.config/hakari.toml index 559599a5007..a18fde09ae7 100644 --- a/.config/hakari.toml +++ b/.config/hakari.toml @@ -29,11 +29,13 @@ workspace-hack-line-style = "workspace-dotted" [final-excludes] workspace-members = [ + "sc-cli", "sc-executor", "sc-executor-common", "sc-executor-polkavm", "sc-executor-wasmtime", "sc-mixnet", + "sc-service", "sp-allocator", "sp-runtime-interface-proc-macro", "sp-wasm-interface", @@ -80,9 +82,7 @@ third-party = [ { name = "pallet-utility", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" }, { name = "pallet-vesting", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" }, { name = "pallet-whitelist", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" }, - { name = "sc-cli", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" }, { name = "sc-client-db", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" }, - { name = "sc-service", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" }, { name = "sp-runtime", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" }, { name = "sp-staking", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" }, ] diff --git a/.github/ansible/tasks/docker/docker-compose-vara.yaml.j2 b/.github/ansible/tasks/docker/docker-compose-vara.yaml.j2 index 095907daada..95795e08fcf 100644 --- a/.github/ansible/tasks/docker/docker-compose-vara.yaml.j2 +++ b/.github/ansible/tasks/docker/docker-compose-vara.yaml.j2 @@ -19,7 +19,7 @@ services: {% if connect == "yes" %} --in-peers 100 --out-peers 100 {% endif %} {% if rpc == "yes" %} --unsafe-ws-external --ws-max-connections='1000' --unsafe-rpc-external --rpc-cors all {% endif %} {% if archive_node == "yes" %} --ws-max-connections='1000' --unsafe-ws-external --unsafe-rpc-external --pruning archive --rpc-cors all {% endif %} - + {% if rpc_method_limits is defined %}{% for rpc_method_limit in rpc_method_limits %} --rpc-method-limit={{ rpc_method_limit }}{% endfor %}{% endif %} {% if loki is defined and loki == "yes" and loki_url is defined %} logging: driver: loki diff --git a/.github/ansible/tasks/docker/docker-compose.yaml.j2 b/.github/ansible/tasks/docker/docker-compose.yaml.j2 index 6a7d53ae7bb..c101654f272 100644 --- a/.github/ansible/tasks/docker/docker-compose.yaml.j2 +++ b/.github/ansible/tasks/docker/docker-compose.yaml.j2 @@ -21,6 +21,7 @@ services: {% if rpc is defined and unsafe == "no" and archive_node == "yes" %} --ws-external --rpc-external --pruning archive {% endif %} {% if rpc is defined %} --rpc-methods Unsafe --rpc-cors all {% endif %} {% if archive_node is defined and archive_node == "yes" and validator is defined and validator == "yes" %} --unsafe-ws-external --rpc-cors all --rpc-methods Unsafe {% endif %} + {% if rpc_method_limits is defined %}{% for rpc_method_limit in rpc_method_limits %} --rpc-method-limit={{ rpc_method_limit }} {% endfor %}{% endif %} {% if loki is defined and loki == "yes" and loki_url is defined %} logging: diff --git a/Cargo.lock b/Cargo.lock index 7fbd5886d23..9024720362f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -694,7 +694,7 @@ dependencies = [ "alloy-rlp", "alloy-serde", "alloy-sol-types", - "itertools 0.14.0", + "itertools 0.13.0", "serde", "serde_json", "serde_with", @@ -850,7 +850,7 @@ checksum = "23ecd298fb0e6a31bd90c72efba2728e3d7569f1c89a3719576fb0e4e40b0fc8" dependencies = [ "alloy-json-rpc", "alloy-transport", - "itertools 0.14.0", + "itertools 0.13.0", "reqwest 0.13.2", "serde_json", "tower 0.5.2", @@ -965,7 +965,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -976,7 +976,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2484,7 +2484,7 @@ version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" dependencies = [ - "bitcoin_hashes 0.13.0", + "bitcoin_hashes 0.14.1", ] [[package]] @@ -3366,7 +3366,7 @@ checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ "serde", "termcolor", - "unicode-width 0.2.2", + "unicode-width 0.1.14", ] [[package]] @@ -3433,7 +3433,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -5647,7 +5647,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -9843,7 +9843,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi 0.5.2", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -12391,7 +12391,7 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9224be3459a0c1d6e9b0f42ab0e76e98b29aef5aba33c0487dfcf47ea08b5150" dependencies = [ - "proc-macro-crate 1.1.3", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", "syn 1.0.109", @@ -12403,7 +12403,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -14780,7 +14780,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", - "heck 0.4.1", + "heck 0.5.0", "itertools 0.12.1", "log", "multimap 0.10.1", @@ -14800,8 +14800,8 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ - "heck 0.4.1", - "itertools 0.14.0", + "heck 0.5.0", + "itertools 0.13.0", "log", "multimap 0.10.1", "once_cell", @@ -14847,7 +14847,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.13.0", "proc-macro2", "quote", "syn 2.0.114", @@ -15853,7 +15853,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -15866,7 +15866,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -15969,7 +15969,7 @@ dependencies = [ "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs 0.26.11", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -15990,7 +15990,7 @@ dependencies = [ "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs 1.0.5", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -16166,14 +16166,14 @@ dependencies = [ [[package]] name = "sc-cli" version = "0.47.0" -source = "git+https://github.com/paritytech/polkadot-sdk.git?rev=298f676c91d64f15f38ea7fd78f125c5889ab09c#298f676c91d64f15f38ea7fd78f125c5889ab09c" dependencies = [ "array-bytes", "chrono", "clap 4.5.54", "fdlimit", "futures", - "itertools 0.11.0", + "futures-timer", + "itertools 0.13.0", "libp2p-identity", "log", "names", @@ -16199,8 +16199,10 @@ dependencies = [ "sp-keystore", "sp-panic-handler", "sp-runtime", + "sp-tracing", "sp-version", - "thiserror 1.0.69", + "tempfile", + "thiserror 2.0.17", "tokio", ] @@ -16459,6 +16461,7 @@ dependencies = [ "sp-version", "sp-wasm-interface", "tracing", + "wat", ] [[package]] @@ -16842,11 +16845,11 @@ dependencies = [ [[package]] name = "sc-rpc-server" version = "17.1.2" -source = "git+https://github.com/paritytech/polkadot-sdk.git?rev=298f676c91d64f15f38ea7fd78f125c5889ab09c#298f676c91d64f15f38ea7fd78f125c5889ab09c" dependencies = [ "dyn-clone", "forwarded-header-value", "futures", + "gear-workspace-hack", "governor", "http 1.4.0", "http-body-util", @@ -16910,7 +16913,6 @@ dependencies = [ [[package]] name = "sc-service" version = "0.46.0" -source = "git+https://github.com/paritytech/polkadot-sdk.git?rev=298f676c91d64f15f38ea7fd78f125c5889ab09c#298f676c91d64f15f38ea7fd78f125c5889ab09c" dependencies = [ "async-trait", "directories", @@ -16964,8 +16966,10 @@ dependencies = [ "sp-version", "static_init", "substrate-prometheus-endpoint", + "substrate-test-runtime", + "substrate-test-runtime-client", "tempfile", - "thiserror 1.0.69", + "thiserror 2.0.17", "tokio", "tracing", "tracing-futures", @@ -18265,6 +18269,22 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "sp-consensus-aura" +version = "0.40.0" +source = "git+https://github.com/paritytech/polkadot-sdk.git?rev=298f676c91d64f15f38ea7fd78f125c5889ab09c#298f676c91d64f15f38ea7fd78f125c5889ab09c" +dependencies = [ + "async-trait", + "parity-scale-codec", + "scale-info", + "sp-api", + "sp-application-crypto", + "sp-consensus-slots", + "sp-inherents", + "sp-runtime", + "sp-timestamp", +] + [[package]] name = "sp-consensus-babe" version = "0.40.0" @@ -19249,6 +19269,68 @@ dependencies = [ "tokio", ] +[[package]] +name = "substrate-test-runtime" +version = "2.0.0" +source = "git+https://github.com/paritytech/polkadot-sdk.git?rev=298f676c91d64f15f38ea7fd78f125c5889ab09c#298f676c91d64f15f38ea7fd78f125c5889ab09c" +dependencies = [ + "array-bytes", + "frame-executive", + "frame-metadata-hash-extension", + "frame-support", + "frame-system", + "frame-system-rpc-runtime-api", + "log", + "pallet-babe", + "pallet-balances", + "pallet-timestamp", + "parity-scale-codec", + "sc-service", + "scale-info", + "serde_json", + "sp-api", + "sp-application-crypto", + "sp-block-builder", + "sp-consensus-aura", + "sp-consensus-babe", + "sp-consensus-grandpa", + "sp-core", + "sp-crypto-hashing 0.1.0 (git+https://github.com/paritytech/polkadot-sdk.git?rev=298f676c91d64f15f38ea7fd78f125c5889ab09c)", + "sp-externalities", + "sp-genesis-builder", + "sp-inherents", + "sp-io", + "sp-keyring", + "sp-offchain", + "sp-runtime", + "sp-session", + "sp-state-machine", + "sp-transaction-pool", + "sp-trie", + "sp-version", + "substrate-wasm-builder", + "tracing", + "trie-db", +] + +[[package]] +name = "substrate-test-runtime-client" +version = "2.0.0" +source = "git+https://github.com/paritytech/polkadot-sdk.git?rev=298f676c91d64f15f38ea7fd78f125c5889ab09c#298f676c91d64f15f38ea7fd78f125c5889ab09c" +dependencies = [ + "futures", + "sc-block-builder", + "sc-client-api", + "sc-consensus", + "sp-api", + "sp-blockchain", + "sp-consensus", + "sp-core", + "sp-runtime", + "substrate-test-client", + "substrate-test-runtime", +] + [[package]] name = "substrate-wasm-builder" version = "24.0.2" @@ -19608,7 +19690,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.3", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -21590,7 +21672,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 53de5cc146c..d66b9fe45cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,9 @@ exclude = [ members = [ # substrate + "substrate/cli", + "substrate/rpc-servers", + "substrate/service", "substrate/runtime-executor", "substrate/runtime-executor/common", "substrate/runtime-executor/polkavm", @@ -168,6 +171,7 @@ bs58 = { version = "0.5.1", default-features = false } build-helper = "0.1.1" cargo_toml = "0.21.0" cargo_metadata = "0.20.0" +chrono = { version = "0.4.31" } clap = "4.5.8" colored = "2.1.0" console = "0.15.8" @@ -181,18 +185,25 @@ derive_more = { version = "2.0.1", default-features = false, features = [ auto_impl = "1.3.0" dirs = "4.0.0" dyn-clonable = "0.9.0" +dyn-clone = { version = "1.0.16" } enum-iterator = "1.5.0" environmental = "1.1.3" +exit-future = { version = "0.2.0" } expander = "2.0.0" +fdlimit = { version = "0.3.0" } filetime = "0.2.16" flate2 = "1" +forwarded-header-value = { version = "0.1.1" } futures = { version = "0.3", default-features = false } futures-timer = "3.0.3" future-timing = "0.1.0" # measure the futures execution +governor = { version = "0.6.0" } hashbrown = "0.14.5" hash-db = { version = "0.16.0", default-features = false } hex = { version = "0.4.3", default-features = false } hex-literal = "0.4.1" +http = { version = "1.1" } +http-body-util = { version = "0.1.2", default-features = false } impl-trait-for-tuples = "0.2.2" impl-serde = "0.4.0" Inflector = "0.11.4" @@ -204,12 +215,14 @@ log = { version = "0.4.22", default-features = false } merkleized-metadata = "0.1.0" mixnet = "0.7.0" multiaddr = "0.18.1" +names = { version = "0.14.0", default-features = false } num_enum = { version = "0.6.1", default-features = false } codec = { package = "parity-scale-codec", version = "3.7.5", default-features = false } numerated = { version = "2.0.1", default-features = false } parity-wasm = "0.45.0" parity-scale-codec = { version = "3.7.5", default-features = false } parking_lot = "0.12.3" +pin-project = { version = "1.1.3" } path-clean = "1.0.1" primitive-types = { version = "0.12.2", default-features = false } proc-macro-crate = "3.0.0" @@ -223,6 +236,7 @@ rayon = { version = "1.11.0" } regex = "^1.9" region = "3.0.2" reqwest = { version = "0.12.8", default-features = false } +rpassword = { version = "7.0.0" } scale-info = { version = "2.11", default-features = false } scale-decode = "0.16.0" scale-encode = "0.10.0" @@ -262,6 +276,7 @@ polkavm = { version = "0.9.3", default-features = false } rustix = { version = "1.0.8", default-features = false } strum = { version = "0.26.2", default-features = false } tracing = { version = "0.1.40", default-features = false } +tracing-futures = { version = "0.2.4" } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } assert_matches = "1.5.0" nonempty = { version = "0.12.0", default-features = false } @@ -478,29 +493,39 @@ sc-consensus-slots = { version = "0.44.0", git = "https://github.com/paritytech/ sp-crypto-ec-utils = { version = "0.14.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c", default-features = false } sp-debug-derive = { version = "14.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c", default-features = false } sc-chain-spec = { version = "38.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } -sc-cli = { version = "0.47.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } +sc-cli = { path = "substrate/cli" } sc-client-api = { version = "37.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } +sc-client-db = { version = "0.44.1", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c", default-features = false } sc-executor = { path = "substrate/runtime-executor" } sc-executor-common = { path = "substrate/runtime-executor/common" } sc-executor-polkavm = { path = "substrate/runtime-executor/polkavm" } sc-executor-wasmtime = { path = "substrate/runtime-executor/wasmtime" } +sc-informant = { version = "0.44.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c", default-features = false } +sc-keystore = { version = "33.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c", default-features = false } sc-mixnet = { path = "substrate/sc-mixnet", default-features = false } sc-consensus-grandpa = { version = "0.30.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } sc-consensus-grandpa-rpc = { version = "0.30.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } sc-network = { version = "0.45.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } +sc-network-common = { version = "0.44.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c", default-features = false } sc-network-sync = { version = "0.44.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } +sc-network-light = { version = "0.44.1", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c", default-features = false } sc-network-types = { version = "0.12.1", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } +sc-network-transactions = { version = "0.44.1", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c", default-features = false } sc-offchain = { version = "40.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } sc-proposer-metrics = { version = "0.18.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } -sc-service = { version = "0.46.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } +sc-service = { path = "substrate/service" } sc-telemetry = { version = "25.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } sc-rpc = { version = "40.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } +sc-rpc-api = { version = "0.44.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c", default-features = false } +sc-rpc-server = { path = "substrate/rpc-servers" } +sc-rpc-spec-v2 = { version = "0.45.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c", default-features = false } sc-runtime-test = { version = "2.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } sc-sync-state-rpc = { version = "0.45.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } sc-sysinfo = { version = "38.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } sc-transaction-pool = { version = "37.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } sc-transaction-pool-api = { version = "37.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } sc-tracing = { version = "37.0.1", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } +sc-utils = { version = "17.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c", default-features = false } sp-allocator = { path = "substrate/sp-allocator", default-features = false } sp-api = { version = "34.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c", default-features = false } sp-authority-discovery = { version = "34.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c", default-features = false } @@ -545,6 +570,8 @@ substrate-frame-rpc-system = { version = "39.0.0", git = "https://github.com/par substrate-rpc-client = { version = "0.44.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } substrate-state-trie-migration-rpc = { version = "38.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } substrate-test-client = { version = "2.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } +substrate-test-runtime = { version = "2.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } +substrate-test-runtime-client = { version = "2.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "298f676c91d64f15f38ea7fd78f125c5889ab09c" } substrate-wasm-builder = { path = "substrate/substrate-wasm-builder" } # Examples @@ -735,6 +762,9 @@ wasm-instrument = { version = "0.4.0", git = "https://github.com/gear-tech/wasm- wasm-smith = { version = "0.230", git = "https://github.com/gear-tech/wasm-tools", branch = "gear-stable-1.230" } [patch."https://github.com/paritytech/polkadot-sdk.git"] +sc-cli = { path = "substrate/cli" } +sc-rpc-server = { path = "substrate/rpc-servers" } +sc-service = { path = "substrate/service" } sc-executor = { path = "substrate/runtime-executor" } sc-executor-common = { path = "substrate/runtime-executor/common" } sc-executor-polkavm = { path = "substrate/runtime-executor/polkavm" } diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index cabf140e2c4..9aa1a01d779 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -9,6 +9,9 @@ Some local crates contain copied or modified source files from `paritytech/polka - `substrate/runtime-executor/polkavm` (`sc-executor-polkavm`, published by Gear as `gsc-executor-polkavm`) - `substrate/runtime-executor/wasmtime` (`sc-executor-wasmtime`, published by Gear as `gsc-executor-wasmtime`) - `substrate/runtime-executor` (`sc-executor`, not published by Gear) +- `substrate/cli` (`sc-cli`, not published by Gear) +- `substrate/rpc-servers` (`sc-rpc-server`, not published by Gear) +- `substrate/service` (`sc-service`, not published by Gear) - `substrate/substrate-wasm-builder` (`substrate-wasm-builder`, published by Gear as `gsubstrate-wasm-builder`) Source reference: diff --git a/_typos.toml b/_typos.toml index 5cb787a4705..5067c3ab9e6 100644 --- a/_typos.toml +++ b/_typos.toml @@ -13,7 +13,9 @@ extend-ignore-identifiers-re = [ "instr_\\w+", "i(32|64)\\w+", # `sp-state-machine` crate - "OverlayedChanges" + "OverlayedChanges", + # generic backend type parameter + "^BA$", ] [files] diff --git a/substrate/README.md b/substrate/README.md index 907448d0cc5..3f1c93de587 100644 --- a/substrate/README.md +++ b/substrate/README.md @@ -16,6 +16,9 @@ Local Cargo package names intentionally stay compatible with upstream package na | `substrate/runtime-executor/polkavm` | `sc-executor-polkavm` | `gsc-executor-polkavm` | GPL-3.0-or-later WITH Classpath-exception-2.0 | | `substrate/runtime-executor/wasmtime` | `sc-executor-wasmtime` | `gsc-executor-wasmtime` | GPL-3.0-or-later WITH Classpath-exception-2.0 | | `substrate/runtime-executor` | `sc-executor` | not published by Gear | GPL-3.0-or-later WITH Classpath-exception-2.0 | +| `substrate/cli` | `sc-cli` | not published by Gear | GPL-3.0-or-later WITH Classpath-exception-2.0 | +| `substrate/rpc-servers` | `sc-rpc-server` | not published by Gear | GPL-3.0-or-later WITH Classpath-exception-2.0 | +| `substrate/service` | `sc-service` | not published by Gear | GPL-3.0-or-later WITH Classpath-exception-2.0 | | `substrate/substrate-wasm-builder` | `substrate-wasm-builder` | `gsubstrate-wasm-builder` | Apache-2.0 | ## Gear Compatibility Crates diff --git a/substrate/cli/Cargo.toml b/substrate/cli/Cargo.toml new file mode 100644 index 00000000000..8edc1a923aa --- /dev/null +++ b/substrate/cli/Cargo.toml @@ -0,0 +1,76 @@ +[package] +name = "sc-cli" +version = "0.47.0" +authors.workspace = true +description = "Substrate CLI interface." +edition = "2021" +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" +homepage.workspace = true +repository.workspace = true +readme = "README.md" + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +array-bytes = { workspace = true, default-features = true } +chrono = { workspace = true } +clap = { features = ["derive", "string", "wrap_help"], workspace = true } +fdlimit = { workspace = true } +futures = { workspace = true, default-features = true } +itertools = { workspace = true, default-features = true } +libp2p-identity = { features = ["ed25519", "peerid"], workspace = true } +log = { workspace = true, default-features = true } +names = { workspace = true } +codec = { workspace = true, default-features = true } +rand = { workspace = true, default-features = true } +regex = { workspace = true } +rpassword = { workspace = true } +serde = { workspace = true, default-features = true } +serde_json = { workspace = true, default-features = true } +thiserror = { workspace = true, default-features = true } +# personal fork here as workaround for: https://github.com/rust-bitcoin/rust-bip39/pull/64 +bip39 = { package = "parity-bip39", version = "2.0.1", features = ["rand"] } +tokio = { features = ["parking_lot", "rt-multi-thread", "signal"], workspace = true, default-features = true } +sc-client-api.workspace = true +sc-client-api.default-features = true +sc-client-db.workspace = true +sc-keystore.workspace = true +sc-keystore.default-features = true +sc-mixnet.workspace = true +sc-mixnet.default-features = true +sc-network.workspace = true +sc-network.default-features = true +sc-service.workspace = true +sc-telemetry.workspace = true +sc-telemetry.default-features = true +sc-tracing.workspace = true +sc-tracing.default-features = true +sc-utils.workspace = true +sc-utils.default-features = true +sp-blockchain.workspace = true +sp-blockchain.default-features = true +sp-core.workspace = true +sp-core.default-features = true +sp-keyring.workspace = true +sp-keyring.default-features = true +sp-keystore.workspace = true +sp-keystore.default-features = true +sp-panic-handler.workspace = true +sp-panic-handler.default-features = true +sp-runtime.workspace = true +sp-runtime.default-features = true +sp-version.workspace = true +sp-version.default-features = true + +[dev-dependencies] +tempfile = { workspace = true } +futures-timer = { workspace = true } +sp-tracing = { workspace = true, default-features = true } + +[features] +default = ["rocksdb"] +rocksdb = ["sc-client-db/rocksdb"] diff --git a/substrate/cli/README.md b/substrate/cli/README.md new file mode 100644 index 00000000000..cf99c0b1a06 --- /dev/null +++ b/substrate/cli/README.md @@ -0,0 +1,8 @@ +Substrate CLI library. + +License: GPL-3.0-or-later WITH Classpath-exception-2.0 + + +## Release + +Polkadot SDK stable2409 diff --git a/substrate/cli/src/arg_enums.rs b/substrate/cli/src/arg_enums.rs new file mode 100644 index 00000000000..009d370c92d --- /dev/null +++ b/substrate/cli/src/arg_enums.rs @@ -0,0 +1,320 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Definitions of [`ValueEnum`] types. + +use clap::ValueEnum; +use std::str::FromStr; + +/// The instantiation strategy to use in compiled mode. +#[derive(Debug, Clone, Copy, ValueEnum)] +#[value(rename_all = "kebab-case")] +pub enum WasmtimeInstantiationStrategy { + /// Pool the instances to avoid initializing everything from scratch + /// on each instantiation. Use copy-on-write memory when possible. + PoolingCopyOnWrite, + + /// Recreate the instance from scratch on every instantiation. + /// Use copy-on-write memory when possible. + RecreateInstanceCopyOnWrite, + + /// Pool the instances to avoid initializing everything from scratch + /// on each instantiation. + Pooling, + + /// Recreate the instance from scratch on every instantiation. Very slow. + RecreateInstance, +} + +/// The default [`WasmtimeInstantiationStrategy`]. +pub const DEFAULT_WASMTIME_INSTANTIATION_STRATEGY: WasmtimeInstantiationStrategy = + WasmtimeInstantiationStrategy::PoolingCopyOnWrite; + +/// How to execute Wasm runtime code. +#[derive(Debug, Clone, Copy, ValueEnum)] +#[value(rename_all = "kebab-case")] +pub enum WasmExecutionMethod { + /// Uses an interpreter which now is deprecated. + #[clap(name = "interpreted-i-know-what-i-do")] + Interpreted, + /// Uses a compiled runtime. + Compiled, +} + +impl std::fmt::Display for WasmExecutionMethod { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Interpreted => write!(f, "Interpreted"), + Self::Compiled => write!(f, "Compiled"), + } + } +} + +/// Converts the execution method and instantiation strategy command line arguments +/// into an execution method which can be used internally. +pub fn execution_method_from_cli( + execution_method: WasmExecutionMethod, + instantiation_strategy: WasmtimeInstantiationStrategy, +) -> sc_service::config::WasmExecutionMethod { + if let WasmExecutionMethod::Interpreted = execution_method { + log::warn!( + "`interpreted-i-know-what-i-do` is deprecated and will be removed in the future. Defaults to `compiled` execution mode." + ); + } + + sc_service::config::WasmExecutionMethod::Compiled { + instantiation_strategy: match instantiation_strategy { + WasmtimeInstantiationStrategy::PoolingCopyOnWrite => { + sc_service::config::WasmtimeInstantiationStrategy::PoolingCopyOnWrite + } + WasmtimeInstantiationStrategy::RecreateInstanceCopyOnWrite => { + sc_service::config::WasmtimeInstantiationStrategy::RecreateInstanceCopyOnWrite + } + WasmtimeInstantiationStrategy::Pooling => { + sc_service::config::WasmtimeInstantiationStrategy::Pooling + } + WasmtimeInstantiationStrategy::RecreateInstance => { + sc_service::config::WasmtimeInstantiationStrategy::RecreateInstance + } + }, + } +} + +/// The default [`WasmExecutionMethod`]. +pub const DEFAULT_WASM_EXECUTION_METHOD: WasmExecutionMethod = WasmExecutionMethod::Compiled; + +#[allow(missing_docs)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum)] +#[value(rename_all = "kebab-case")] +pub enum TracingReceiver { + /// Output the tracing records using the log. + Log, +} + +impl From for sc_tracing::TracingReceiver { + fn from(val: TracingReceiver) -> Self { + match val { + TracingReceiver::Log => sc_tracing::TracingReceiver::Log, + } + } +} + +/// The type of the node key. +#[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum)] +#[value(rename_all = "kebab-case")] +pub enum NodeKeyType { + /// Use ed25519. + Ed25519, +} + +/// The crypto scheme to use. +#[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum)] +#[value(rename_all = "kebab-case")] +pub enum CryptoScheme { + /// Use ed25519. + Ed25519, + /// Use sr25519. + Sr25519, + /// Use ecdsa. + Ecdsa, +} + +/// The type of the output format. +#[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum)] +#[value(rename_all = "kebab-case")] +pub enum OutputType { + /// Output as json. + Json, + /// Output as text. + Text, +} + +/// How to execute blocks +#[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum)] +#[value(rename_all = "kebab-case")] +pub enum ExecutionStrategy { + /// Execute with native build (if available, WebAssembly otherwise). + Native, + /// Only execute with the WebAssembly build. + Wasm, + /// Execute with both native (where available) and WebAssembly builds. + Both, + /// Execute with the native build if possible; if it fails, then execute with WebAssembly. + NativeElseWasm, +} + +/// Available RPC methods. +#[allow(missing_docs)] +#[derive(Debug, Copy, Clone, PartialEq, ValueEnum)] +#[value(rename_all = "kebab-case")] +pub enum RpcMethods { + /// Expose every RPC method only when RPC is listening on `localhost`, + /// otherwise serve only safe RPC methods. + Auto, + /// Allow only a safe subset of RPC methods. + Safe, + /// Expose every RPC method (even potentially unsafe ones). + Unsafe, +} + +impl FromStr for RpcMethods { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "safe" => Ok(RpcMethods::Safe), + "unsafe" => Ok(RpcMethods::Unsafe), + "auto" => Ok(RpcMethods::Auto), + invalid => Err(format!("Invalid rpc methods {invalid}")), + } + } +} + +impl From for sc_service::config::RpcMethods { + fn from(val: RpcMethods) -> Self { + match val { + RpcMethods::Auto => sc_service::config::RpcMethods::Auto, + RpcMethods::Safe => sc_service::config::RpcMethods::Safe, + RpcMethods::Unsafe => sc_service::config::RpcMethods::Unsafe, + } + } +} + +/// CORS setting +/// +/// The type is introduced to overcome `Option>` handling of `clap`. +#[derive(Clone, Debug)] +pub enum Cors { + /// All hosts allowed. + All, + /// Only hosts on the list are allowed. + List(Vec), +} + +impl From for Option> { + fn from(cors: Cors) -> Self { + match cors { + Cors::All => None, + Cors::List(list) => Some(list), + } + } +} + +impl FromStr for Cors { + type Err = crate::Error; + + fn from_str(s: &str) -> Result { + let mut is_all = false; + let mut origins = Vec::new(); + for part in s.split(',') { + match part { + "all" | "*" => { + is_all = true; + break; + } + other => origins.push(other.to_owned()), + } + } + + if is_all { + Ok(Cors::All) + } else { + Ok(Cors::List(origins)) + } + } +} + +/// Database backend +#[derive(Debug, Clone, PartialEq, Copy, clap::ValueEnum)] +#[value(rename_all = "lower")] +pub enum Database { + /// Facebooks RocksDB + #[cfg(feature = "rocksdb")] + RocksDb, + /// ParityDb. + ParityDb, + /// Detect whether there is an existing database. Use it, if there is, if not, create new + /// instance of ParityDb + Auto, + /// ParityDb. + #[value(name = "paritydb-experimental")] + ParityDbDeprecated, +} + +impl Database { + /// Returns all the variants of this enum to be shown in the cli. + pub const fn variants() -> &'static [&'static str] { + &[ + #[cfg(feature = "rocksdb")] + "rocksdb", + "paritydb", + "paritydb-experimental", + "auto", + ] + } +} + +/// Whether off-chain workers are enabled. +#[allow(missing_docs)] +#[derive(Debug, Clone, ValueEnum)] +#[value(rename_all = "kebab-case")] +pub enum OffchainWorkerEnabled { + /// Always have offchain worker enabled. + Always, + /// Never enable the offchain worker. + Never, + /// Only enable the offchain worker when running as a validator (or collator, if this is a + /// parachain node). + WhenAuthority, +} + +/// Syncing mode. +#[derive(Debug, Clone, Copy, ValueEnum, PartialEq)] +#[value(rename_all = "kebab-case")] +pub enum SyncMode { + /// Full sync. Download and verify all blocks. + Full, + /// Download blocks without executing them. Download latest state with proofs. + Fast, + /// Download blocks without executing them. Download latest state without proofs. + FastUnsafe, + /// Prove finality and download the latest state. + Warp, +} + +impl From for sc_network::config::SyncMode { + fn from(val: SyncMode) -> Self { + match val { + SyncMode::Full => sc_network::config::SyncMode::Full, + SyncMode::Fast => sc_network::config::SyncMode::LightState { + skip_proofs: false, + storage_chain_mode: false, + }, + SyncMode::FastUnsafe => sc_network::config::SyncMode::LightState { + skip_proofs: true, + storage_chain_mode: false, + }, + SyncMode::Warp => sc_network::config::SyncMode::Warp, + } + } +} + +/// Network backend type. +#[derive(Debug, Clone, Copy, ValueEnum, PartialEq)] +#[value(rename_all = "lower")] +pub enum NetworkBackendType { + /// Use libp2p for P2P networking. + Libp2p, + + /// Use litep2p for P2P networking. + Litep2p, +} + +impl From for sc_network::config::NetworkBackendType { + fn from(val: NetworkBackendType) -> Self { + match val { + NetworkBackendType::Libp2p => sc_network::config::NetworkBackendType::Libp2p, + NetworkBackendType::Litep2p => sc_network::config::NetworkBackendType::Litep2p, + } + } +} diff --git a/substrate/cli/src/commands/build_spec_cmd.rs b/substrate/cli/src/commands/build_spec_cmd.rs new file mode 100644 index 00000000000..15544552dc5 --- /dev/null +++ b/substrate/cli/src/commands/build_spec_cmd.rs @@ -0,0 +1,76 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::{ + error, + params::{NodeKeyParams, SharedParams}, + CliConfiguration, +}; +use clap::Parser; +use log::info; +use sc_network::config::build_multiaddr; +use sc_service::{ + config::{MultiaddrWithPeerId, NetworkConfiguration}, + ChainSpec, +}; +use std::io::Write; + +/// The `build-spec` command used to build a specification. +#[derive(Debug, Clone, Parser)] +pub struct BuildSpecCmd { + /// Force raw genesis storage output. + #[arg(long)] + pub raw: bool, + + /// Disable adding the default bootnode to the specification. + /// By default the `/ip4/127.0.0.1/tcp/30333/p2p/NODE_PEER_ID` bootnode is added to the + /// specification when no bootnode exists. + #[arg(long)] + pub disable_default_bootnode: bool, + + #[allow(missing_docs)] + #[clap(flatten)] + pub shared_params: SharedParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub node_key_params: NodeKeyParams, +} + +impl BuildSpecCmd { + /// Run the build-spec command + pub fn run( + &self, + mut spec: Box, + network_config: NetworkConfiguration, + ) -> error::Result<()> { + info!("Building chain spec"); + let raw_output = self.raw; + + if spec.boot_nodes().is_empty() && !self.disable_default_bootnode { + let keys = network_config.node_key.into_keypair()?; + let peer_id = keys.public().to_peer_id(); + let addr = MultiaddrWithPeerId { + multiaddr: build_multiaddr![Ip4([127, 0, 0, 1]), Tcp(30333u16)], + peer_id, + }; + spec.add_boot_node(addr) + } + + let json = sc_service::chain_ops::build_spec(&*spec, raw_output)?; + if std::io::stdout().write_all(json.as_bytes()).is_err() { + let _ = std::io::stderr().write_all(b"Error writing to stdout\n"); + } + Ok(()) + } +} + +impl CliConfiguration for BuildSpecCmd { + fn shared_params(&self) -> &SharedParams { + &self.shared_params + } + + fn node_key_params(&self) -> Option<&NodeKeyParams> { + Some(&self.node_key_params) + } +} diff --git a/substrate/cli/src/commands/chain_info_cmd.rs b/substrate/cli/src/commands/chain_info_cmd.rs new file mode 100644 index 00000000000..db28cf50ba5 --- /dev/null +++ b/substrate/cli/src/commands/chain_info_cmd.rs @@ -0,0 +1,87 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::{CliConfiguration, DatabaseParams, PruningParams, Result as CliResult, SharedParams}; +use codec::{Decode, Encode}; +use sc_client_api::{backend::Backend as BackendT, blockchain::HeaderBackend}; +use sp_blockchain::Info; +use sp_runtime::traits::{Block as BlockT, Header as HeaderT}; +use std::{fmt::Debug, io}; + +/// The `chain-info` subcommand used to output db meta columns information. +#[derive(Debug, Clone, clap::Parser)] +pub struct ChainInfoCmd { + #[allow(missing_docs)] + #[clap(flatten)] + pub pruning_params: PruningParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub shared_params: SharedParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub database_params: DatabaseParams, +} + +/// Serializable `chain-info` subcommand output. +#[derive(Clone, Eq, PartialEq, Debug, Encode, Decode, serde::Serialize)] +struct ChainInfo { + /// Best block hash. + best_hash: B::Hash, + /// Best block number. + best_number: <::Header as HeaderT>::Number, + /// Genesis block hash. + genesis_hash: B::Hash, + /// The head of the finalized chain. + finalized_hash: B::Hash, + /// Last finalized block number. + finalized_number: <::Header as HeaderT>::Number, +} + +impl From> for ChainInfo { + fn from(info: Info) -> Self { + ChainInfo:: { + best_hash: info.best_hash, + best_number: info.best_number, + genesis_hash: info.genesis_hash, + finalized_hash: info.finalized_hash, + finalized_number: info.finalized_number, + } + } +} + +impl ChainInfoCmd { + /// Run the `chain-info` subcommand + pub fn run(&self, config: &sc_service::Configuration) -> CliResult<()> + where + B: BlockT, + { + let db_config = sc_client_db::DatabaseSettings { + trie_cache_maximum_size: config.trie_cache_maximum_size, + state_pruning: config.state_pruning.clone(), + source: config.database.clone(), + blocks_pruning: config.blocks_pruning, + }; + let backend = sc_service::new_db_backend::(db_config)?; + let info: ChainInfo = backend.blockchain().info().into(); + let mut out = io::stdout(); + serde_json::to_writer_pretty(&mut out, &info) + .map_err(|e| format!("Error writing JSON: {}", e))?; + Ok(()) + } +} + +impl CliConfiguration for ChainInfoCmd { + fn shared_params(&self) -> &SharedParams { + &self.shared_params + } + + fn pruning_params(&self) -> Option<&PruningParams> { + Some(&self.pruning_params) + } + + fn database_params(&self) -> Option<&DatabaseParams> { + Some(&self.database_params) + } +} diff --git a/substrate/cli/src/commands/check_block_cmd.rs b/substrate/cli/src/commands/check_block_cmd.rs new file mode 100644 index 00000000000..4e565bd81b3 --- /dev/null +++ b/substrate/cli/src/commands/check_block_cmd.rs @@ -0,0 +1,61 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::{ + error, + params::{BlockNumberOrHash, ImportParams, SharedParams}, + CliConfiguration, +}; +use clap::Parser; +use sc_client_api::{BlockBackend, HeaderBackend}; +use sp_runtime::traits::{Block as BlockT, Header as HeaderT}; +use std::{fmt::Debug, str::FromStr, sync::Arc}; + +/// The `check-block` command used to validate blocks. +#[derive(Debug, Clone, Parser)] +pub struct CheckBlockCmd { + /// Block hash or number. + #[arg(value_name = "HASH or NUMBER")] + pub input: BlockNumberOrHash, + + /// The default number of 64KB pages to ever allocate for Wasm execution. + /// Don't alter this unless you know what you're doing. + #[arg(long, value_name = "COUNT")] + pub default_heap_pages: Option, + + #[allow(missing_docs)] + #[clap(flatten)] + pub shared_params: SharedParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub import_params: ImportParams, +} + +impl CheckBlockCmd { + /// Run the check-block command + pub async fn run(&self, client: Arc, import_queue: IQ) -> error::Result<()> + where + B: BlockT + for<'de> serde::Deserialize<'de>, + C: BlockBackend + HeaderBackend + Send + Sync + 'static, + IQ: sc_service::ImportQueue + 'static, + ::Err: Debug, + <::Number as FromStr>::Err: Debug, + { + let start = std::time::Instant::now(); + sc_service::chain_ops::check_block(client, import_queue, self.input.parse()?).await?; + println!("Completed in {} ms.", start.elapsed().as_millis()); + + Ok(()) + } +} + +impl CliConfiguration for CheckBlockCmd { + fn shared_params(&self) -> &SharedParams { + &self.shared_params + } + + fn import_params(&self) -> Option<&ImportParams> { + Some(&self.import_params) + } +} diff --git a/substrate/cli/src/commands/export_blocks_cmd.rs b/substrate/cli/src/commands/export_blocks_cmd.rs new file mode 100644 index 00000000000..b5391976b86 --- /dev/null +++ b/substrate/cli/src/commands/export_blocks_cmd.rs @@ -0,0 +1,98 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::{ + error, + params::{DatabaseParams, GenericNumber, PruningParams, SharedParams}, + CliConfiguration, +}; +use clap::Parser; +use log::info; +use sc_client_api::{BlockBackend, HeaderBackend, UsageProvider}; +use sc_service::{chain_ops::export_blocks, config::DatabaseSource}; +use sp_runtime::traits::{Block as BlockT, Header as HeaderT}; +use std::{fmt::Debug, fs, io, path::PathBuf, str::FromStr, sync::Arc}; + +/// The `export-blocks` command used to export blocks. +#[derive(Debug, Clone, Parser)] +pub struct ExportBlocksCmd { + /// Output file name or stdout if unspecified. + #[arg()] + pub output: Option, + + /// Specify starting block number. + /// Default is 1. + #[arg(long, value_name = "BLOCK")] + pub from: Option, + + /// Specify last block number. + /// Default is best block. + #[arg(long, value_name = "BLOCK")] + pub to: Option, + + /// Use binary output rather than JSON. + #[arg(long)] + pub binary: bool, + + #[allow(missing_docs)] + #[clap(flatten)] + pub shared_params: SharedParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub pruning_params: PruningParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub database_params: DatabaseParams, +} + +impl ExportBlocksCmd { + /// Run the export-blocks command + pub async fn run( + &self, + client: Arc, + database_config: DatabaseSource, + ) -> error::Result<()> + where + B: BlockT, + C: HeaderBackend + BlockBackend + UsageProvider + 'static, + <::Number as FromStr>::Err: Debug, + { + if let Some(path) = database_config.path() { + info!("DB path: {}", path.display()); + } + + let from = self + .from + .as_ref() + .and_then(|f| f.parse().ok()) + .unwrap_or(1u32); + let to = self.to.as_ref().and_then(|t| t.parse().ok()); + + let binary = self.binary; + + let file: Box = match &self.output { + Some(filename) => Box::new(fs::File::create(filename)?), + None => Box::new(io::stdout()), + }; + + export_blocks(client, file, from.into(), to, binary) + .await + .map_err(Into::into) + } +} + +impl CliConfiguration for ExportBlocksCmd { + fn shared_params(&self) -> &SharedParams { + &self.shared_params + } + + fn pruning_params(&self) -> Option<&PruningParams> { + Some(&self.pruning_params) + } + + fn database_params(&self) -> Option<&DatabaseParams> { + Some(&self.database_params) + } +} diff --git a/substrate/cli/src/commands/export_state_cmd.rs b/substrate/cli/src/commands/export_state_cmd.rs new file mode 100644 index 00000000000..8f9c309351f --- /dev/null +++ b/substrate/cli/src/commands/export_state_cmd.rs @@ -0,0 +1,80 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::{ + error, + params::{BlockNumberOrHash, DatabaseParams, PruningParams, SharedParams}, + CliConfiguration, +}; +use clap::Parser; +use log::info; +use sc_client_api::{HeaderBackend, StorageProvider, UsageProvider}; +use sp_runtime::traits::{Block as BlockT, Header as HeaderT}; +use std::{fmt::Debug, io::Write, str::FromStr, sync::Arc}; + +/// The `export-state` command used to export the state of a given block into +/// a chain spec. +#[derive(Debug, Clone, Parser)] +pub struct ExportStateCmd { + /// Block hash or number. + #[arg(value_name = "HASH or NUMBER")] + pub input: Option, + + #[allow(missing_docs)] + #[clap(flatten)] + pub shared_params: SharedParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub pruning_params: PruningParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub database_params: DatabaseParams, +} + +impl ExportStateCmd { + /// Run the `export-state` command + pub async fn run( + &self, + client: Arc, + mut input_spec: Box, + ) -> error::Result<()> + where + B: BlockT, + C: UsageProvider + StorageProvider + HeaderBackend, + BA: sc_client_api::backend::Backend, + ::Err: Debug, + <::Number as FromStr>::Err: Debug, + { + info!("Exporting raw state..."); + let block_id = self.input.as_ref().map(|b| b.parse()).transpose()?; + let hash = match block_id { + Some(id) => client.expect_block_hash_from_id(&id)?, + None => client.usage_info().chain.best_hash, + }; + let raw_state = sc_service::chain_ops::export_raw_state(client, hash)?; + input_spec.set_storage(raw_state); + + info!("Generating new chain spec..."); + let json = sc_service::chain_ops::build_spec(&*input_spec, true)?; + if std::io::stdout().write_all(json.as_bytes()).is_err() { + let _ = std::io::stderr().write_all(b"Error writing to stdout\n"); + } + Ok(()) + } +} + +impl CliConfiguration for ExportStateCmd { + fn shared_params(&self) -> &SharedParams { + &self.shared_params + } + + fn pruning_params(&self) -> Option<&PruningParams> { + Some(&self.pruning_params) + } + + fn database_params(&self) -> Option<&DatabaseParams> { + Some(&self.database_params) + } +} diff --git a/substrate/cli/src/commands/generate.rs b/substrate/cli/src/commands/generate.rs new file mode 100644 index 00000000000..e22273c5ef2 --- /dev/null +++ b/substrate/cli/src/commands/generate.rs @@ -0,0 +1,72 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Implementation of the `generate` subcommand +use crate::{ + utils::print_from_uri, with_crypto_scheme, CryptoSchemeFlag, Error, KeystoreParams, + NetworkSchemeFlag, OutputTypeFlag, +}; +use bip39::Mnemonic; +use clap::Parser; +use itertools::Itertools; + +/// The `generate` command +#[derive(Debug, Clone, Parser)] +#[command(name = "generate", about = "Generate a random account")] +pub struct GenerateCmd { + /// The number of words in the phrase to generate. One of 12 (default), 15, 18, 21 and 24. + #[arg(short = 'w', long, value_name = "WORDS")] + words: Option, + + #[allow(missing_docs)] + #[clap(flatten)] + pub keystore_params: KeystoreParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub network_scheme: NetworkSchemeFlag, + + #[allow(missing_docs)] + #[clap(flatten)] + pub output_scheme: OutputTypeFlag, + + #[allow(missing_docs)] + #[clap(flatten)] + pub crypto_scheme: CryptoSchemeFlag, +} + +impl GenerateCmd { + /// Run the command + pub fn run(&self) -> Result<(), Error> { + let words = match self.words { + Some(words_count) if [12, 15, 18, 21, 24].contains(&words_count) => Ok(words_count), + Some(_) => Err(Error::Input( + "Invalid number of words given for phrase: must be 12/15/18/21/24".into(), + )), + None => Ok(12), + }?; + let mnemonic = Mnemonic::generate(words) + .map_err(|e| Error::Input(format!("Mnemonic generation failed: {e}")))?; + let password = self.keystore_params.read_password()?; + let output = self.output_scheme.output_type; + + let phrase = mnemonic.words().join(" "); + + with_crypto_scheme!( + self.crypto_scheme.scheme, + print_from_uri(&phrase, password, self.network_scheme.network, output) + ); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generate() { + let generate = GenerateCmd::parse_from(["generate", "--password", "12345"]); + assert!(generate.run().is_ok()) + } +} diff --git a/substrate/cli/src/commands/generate_node_key.rs b/substrate/cli/src/commands/generate_node_key.rs new file mode 100644 index 00000000000..205ca9b4962 --- /dev/null +++ b/substrate/cli/src/commands/generate_node_key.rs @@ -0,0 +1,172 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Implementation of the `generate-node-key` subcommand + +use crate::{build_network_key_dir_or_default, Error, NODE_KEY_ED25519_FILE}; +use clap::{Args, Parser}; +use libp2p_identity::{ed25519, Keypair}; +use sc_service::BasePath; +use std::{ + fs, + io::{self, Write}, + path::PathBuf, +}; + +/// Common arguments across all generate key commands, subkey and node. +#[derive(Debug, Args, Clone)] +pub struct GenerateKeyCmdCommon { + /// Name of file to save secret key to. + /// If not given, the secret key is printed to stdout. + #[arg(long)] + file: Option, + + /// The output is in raw binary format. + /// If not given, the output is written as an hex encoded string. + #[arg(long)] + bin: bool, +} + +/// The `generate-node-key` command +#[derive(Debug, Clone, Parser)] +#[command( + name = "generate-node-key", + about = "Generate a random node key, write it to a file or stdout \ + and write the corresponding peer-id to stderr" +)] +pub struct GenerateNodeKeyCmd { + #[clap(flatten)] + pub common: GenerateKeyCmdCommon, + /// Specify the chain specification. + /// + /// It can be any of the predefined chains like dev, local, staging, polkadot, kusama. + #[arg(long, value_name = "CHAIN_SPEC")] + pub chain: Option, + /// A directory where the key should be saved. If a key already + /// exists in the directory, it won't be overwritten. + #[arg(long, conflicts_with_all = ["file", "default_base_path"])] + base_path: Option, + + /// Save the key in the default directory. If a key already + /// exists in the directory, it won't be overwritten. + #[arg(long, conflicts_with_all = ["base_path", "file"])] + default_base_path: bool, +} + +impl GenerateKeyCmdCommon { + /// Run the command + pub fn run(&self) -> Result<(), Error> { + generate_key(&self.file, self.bin, None, &None, false, None) + } +} + +impl GenerateNodeKeyCmd { + /// Run the command + pub fn run(&self, chain_spec_id: &str, executable_name: &String) -> Result<(), Error> { + generate_key( + &self.common.file, + self.common.bin, + Some(chain_spec_id), + &self.base_path, + self.default_base_path, + Some(executable_name), + ) + } +} + +// Utility function for generating a key based on the provided CLI arguments +// +// `file` - Name of file to save secret key to +// `bin` +fn generate_key( + file: &Option, + bin: bool, + chain_spec_id: Option<&str>, + base_path: &Option, + default_base_path: bool, + executable_name: Option<&String>, +) -> Result<(), Error> { + let keypair = ed25519::Keypair::generate(); + + let secret = keypair.secret(); + + let file_data = if bin { + secret.as_ref().to_owned() + } else { + array_bytes::bytes2hex("", secret).into_bytes() + }; + + match (file, base_path, default_base_path) { + (Some(file), None, false) => fs::write(file, file_data)?, + (None, Some(_), false) | (None, None, true) => { + let network_path = build_network_key_dir_or_default( + base_path.clone().map(BasePath::new), + chain_spec_id.unwrap_or_default(), + executable_name.ok_or(Error::Input("Executable name not provided".into()))?, + ); + + fs::create_dir_all(network_path.as_path())?; + + let key_path = network_path.join(NODE_KEY_ED25519_FILE); + if key_path.exists() { + eprintln!("Skip generation, a key already exists in {:?}", key_path); + return Err(Error::KeyAlreadyExistsInPath(key_path)); + } else { + eprintln!("Generating key in {:?}", key_path); + fs::write(key_path, file_data)? + } + } + (None, None, false) => io::stdout().lock().write_all(&file_data)?, + (_, _, _) => { + // This should not happen, arguments are marked as mutually exclusive. + return Err(Error::Input("Mutually exclusive arguments provided".into())); + } + } + + eprintln!("{}", Keypair::from(keypair).public().to_peer_id()); + + Ok(()) +} + +#[cfg(test)] +pub mod tests { + use crate::DEFAULT_NETWORK_CONFIG_PATH; + + use super::*; + use std::io::Read; + use tempfile::Builder; + + #[test] + fn generate_node_key() { + let mut file = Builder::new().prefix("keyfile").tempfile().unwrap(); + let file_path = file.path().display().to_string(); + let generate = GenerateNodeKeyCmd::parse_from(["generate-node-key", "--file", &file_path]); + assert!(generate.run("test", &String::from("test")).is_ok()); + let mut buf = String::new(); + assert!(file.read_to_string(&mut buf).is_ok()); + assert!(array_bytes::hex2bytes(&buf).is_ok()); + } + + #[test] + fn generate_node_key_base_path() { + let base_dir = Builder::new().prefix("keyfile").tempdir().unwrap(); + let key_path = base_dir + .path() + .join("chains/test_id/") + .join(DEFAULT_NETWORK_CONFIG_PATH) + .join(NODE_KEY_ED25519_FILE); + let base_path = base_dir.path().display().to_string(); + let generate = + GenerateNodeKeyCmd::parse_from(["generate-node-key", "--base-path", &base_path]); + assert!(generate.run("test_id", &String::from("test")).is_ok()); + let buf = fs::read_to_string(key_path.as_path()).unwrap(); + assert!(array_bytes::hex2bytes(&buf).is_ok()); + + assert!(generate.run("test_id", &String::from("test")).is_err()); + let new_buf = fs::read_to_string(key_path).unwrap(); + assert_eq!( + array_bytes::hex2bytes(&new_buf).unwrap(), + array_bytes::hex2bytes(&buf).unwrap() + ); + } +} diff --git a/substrate/cli/src/commands/import_blocks_cmd.rs b/substrate/cli/src/commands/import_blocks_cmd.rs new file mode 100644 index 00000000000..613a43cb954 --- /dev/null +++ b/substrate/cli/src/commands/import_blocks_cmd.rs @@ -0,0 +1,73 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::{ + error, + params::{ImportParams, SharedParams}, + CliConfiguration, +}; +use clap::Parser; +use sc_client_api::HeaderBackend; +use sc_service::chain_ops::import_blocks; +use sp_runtime::traits::Block as BlockT; +use std::{ + fmt::Debug, + fs, + io::{self, Read}, + path::PathBuf, + sync::Arc, +}; + +/// The `import-blocks` command used to import blocks. +#[derive(Debug, Parser)] +pub struct ImportBlocksCmd { + /// Input file or stdin if unspecified. + #[arg()] + pub input: Option, + + /// The default number of 64KB pages to ever allocate for Wasm execution. + /// Don't alter this unless you know what you're doing. + #[arg(long, value_name = "COUNT")] + pub default_heap_pages: Option, + + /// Try importing blocks from binary format rather than JSON. + #[arg(long)] + pub binary: bool, + + #[allow(missing_docs)] + #[clap(flatten)] + pub shared_params: SharedParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub import_params: ImportParams, +} + +impl ImportBlocksCmd { + /// Run the import-blocks command + pub async fn run(&self, client: Arc, import_queue: IQ) -> error::Result<()> + where + C: HeaderBackend + Send + Sync + 'static, + B: BlockT + for<'de> serde::Deserialize<'de>, + IQ: sc_service::ImportQueue + 'static, + { + let file: Box = match &self.input { + Some(filename) => Box::new(fs::File::open(filename)?), + None => Box::new(io::stdin()), + }; + + import_blocks(client, import_queue, file, false, self.binary) + .await + .map_err(Into::into) + } +} + +impl CliConfiguration for ImportBlocksCmd { + fn shared_params(&self) -> &SharedParams { + &self.shared_params + } + + fn import_params(&self) -> Option<&ImportParams> { + Some(&self.import_params) + } +} diff --git a/substrate/cli/src/commands/insert_key.rs b/substrate/cli/src/commands/insert_key.rs new file mode 100644 index 00000000000..d6e60b1bfbe --- /dev/null +++ b/substrate/cli/src/commands/insert_key.rs @@ -0,0 +1,153 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Implementation of the `insert` subcommand + +use crate::{ + utils, with_crypto_scheme, CryptoScheme, Error, KeystoreParams, SharedParams, SubstrateCli, +}; +use clap::Parser; +use sc_keystore::LocalKeystore; +use sc_service::config::{BasePath, KeystoreConfig}; +use sp_core::crypto::{KeyTypeId, SecretString}; +use sp_keystore::KeystorePtr; + +/// The `insert` command +#[derive(Debug, Clone, Parser)] +#[command(name = "insert", about = "Insert a key to the keystore of a node.")] +pub struct InsertKeyCmd { + /// The secret key URI. + /// If the value is a file, the file content is used as URI. + /// If not given, you will be prompted for the URI. + #[arg(long)] + suri: Option, + + /// Key type, examples: "gran", or "imon". + #[arg(long)] + key_type: String, + + #[allow(missing_docs)] + #[clap(flatten)] + pub shared_params: SharedParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub keystore_params: KeystoreParams, + + /// The cryptography scheme that should be used to generate the key out of the given URI. + #[arg(long, value_name = "SCHEME", value_enum, ignore_case = true)] + pub scheme: CryptoScheme, +} + +impl InsertKeyCmd { + /// Run the command + pub fn run(&self, cli: &C) -> Result<(), Error> { + let suri = utils::read_uri(self.suri.as_ref())?; + let base_path = self + .shared_params + .base_path()? + .unwrap_or_else(|| BasePath::from_project("", "", &C::executable_name())); + let chain_id = self.shared_params.chain_id(self.shared_params.is_dev()); + let chain_spec = cli.load_spec(&chain_id)?; + let config_dir = base_path.config_dir(chain_spec.id()); + + let (keystore, public) = match self.keystore_params.keystore_config(&config_dir)? { + KeystoreConfig::Path { path, password } => { + let public = with_crypto_scheme!(self.scheme, to_vec(&suri, password.clone()))?; + let keystore: KeystorePtr = LocalKeystore::open(path, password)?.into(); + (keystore, public) + } + _ => unreachable!("keystore_config always returns path and password; qed"), + }; + + let key_type = + KeyTypeId::try_from(self.key_type.as_str()).map_err(|_| Error::KeyTypeInvalid)?; + + keystore + .insert(key_type, &suri, &public[..]) + .map_err(|_| Error::KeystoreOperation)?; + + Ok(()) + } +} + +fn to_vec(uri: &str, pass: Option) -> Result, Error> { + let p = utils::pair_from_suri::

(uri, pass)?; + Ok(p.public().as_ref().to_vec()) +} + +#[cfg(test)] +mod tests { + use super::*; + use sc_service::{ChainSpec, ChainType, GenericChainSpec, NoExtension}; + use sp_core::{sr25519::Pair, ByteArray, Pair as _}; + use sp_keystore::Keystore; + use tempfile::TempDir; + + struct Cli; + + impl SubstrateCli for Cli { + fn impl_name() -> String { + "test".into() + } + + fn impl_version() -> String { + "2.0".into() + } + + fn description() -> String { + "test".into() + } + + fn support_url() -> String { + "test.test".into() + } + + fn copyright_start_year() -> i32 { + 2021 + } + + fn author() -> String { + "test".into() + } + + fn load_spec(&self, _: &str) -> std::result::Result, String> { + let builder = + GenericChainSpec::::builder(Default::default(), NoExtension::None); + Ok(Box::new( + builder + .with_name("test") + .with_id("test_id") + .with_chain_type(ChainType::Development) + .with_genesis_config_patch(Default::default()) + .build(), + )) + } + } + + #[test] + fn insert_with_custom_base_path() { + let path = TempDir::new().unwrap(); + let path_str = format!("{}", path.path().display()); + let (key, uri, _) = Pair::generate_with_phrase(None); + + let inspect = InsertKeyCmd::parse_from([ + "insert-key", + "-d", + &path_str, + "--key-type", + "test", + "--suri", + &uri, + "--scheme=sr25519", + ]); + assert!(inspect.run(&Cli).is_ok()); + + let keystore = LocalKeystore::open( + path.path().join("chains").join("test_id").join("keystore"), + None, + ) + .unwrap(); + assert!(keystore.has_keys(&[(key.public().to_raw_vec(), KeyTypeId(*b"test"))])); + } +} diff --git a/substrate/cli/src/commands/inspect_key.rs b/substrate/cli/src/commands/inspect_key.rs new file mode 100644 index 00000000000..292821e3c74 --- /dev/null +++ b/substrate/cli/src/commands/inspect_key.rs @@ -0,0 +1,257 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Implementation of the `inspect` subcommand + +use crate::{ + utils::{self, print_from_public, print_from_uri}, + with_crypto_scheme, CryptoSchemeFlag, Error, KeystoreParams, NetworkSchemeFlag, OutputTypeFlag, +}; +use clap::Parser; +use sp_core::crypto::{ExposeSecret, SecretString, SecretUri, Ss58Codec}; +use std::str::FromStr; + +/// The `inspect` command +#[derive(Debug, Parser)] +#[command( + name = "inspect", + about = "Gets a public key and a SS58 address from the provided Secret URI" +)] +pub struct InspectKeyCmd { + /// A Key URI to be inspected. May be a secret seed, secret URI + /// (with derivation paths and password), SS58, public URI or a hex encoded public key. + /// If it is a hex encoded public key, `--public` needs to be given as argument. + /// If the given value is a file, the file content will be used + /// as URI. + /// If omitted, you will be prompted for the URI. + uri: Option, + + /// Is the given `uri` a hex encoded public key? + #[arg(long)] + public: bool, + + #[allow(missing_docs)] + #[clap(flatten)] + pub keystore_params: KeystoreParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub network_scheme: NetworkSchemeFlag, + + #[allow(missing_docs)] + #[clap(flatten)] + pub output_scheme: OutputTypeFlag, + + #[allow(missing_docs)] + #[clap(flatten)] + pub crypto_scheme: CryptoSchemeFlag, + + /// Expect that `--uri` has the given public key/account-id. + /// If `--uri` has any derivations, the public key is checked against the base `uri`, i.e. the + /// `uri` without any derivation applied. However, if `uri` has a password or there is one + /// given by `--password`, it will be used to decrypt `uri` before comparing the public + /// key/account-id. + /// If there is no derivation in `--uri`, the public key will be checked against the public key + /// of `--uri` directly. + #[arg(long, conflicts_with = "public")] + pub expect_public: Option, +} + +impl InspectKeyCmd { + /// Run the command + pub fn run(&self) -> Result<(), Error> { + let uri = utils::read_uri(self.uri.as_ref())?; + let password = self.keystore_params.read_password()?; + + if self.public { + with_crypto_scheme!( + self.crypto_scheme.scheme, + print_from_public( + &uri, + self.network_scheme.network, + self.output_scheme.output_type, + ) + )?; + } else { + if let Some(ref expect_public) = self.expect_public { + with_crypto_scheme!( + self.crypto_scheme.scheme, + expect_public_from_phrase(expect_public, &uri, password.as_ref()) + )?; + } + + with_crypto_scheme!( + self.crypto_scheme.scheme, + print_from_uri( + &uri, + password, + self.network_scheme.network, + self.output_scheme.output_type, + ) + ); + } + + Ok(()) + } +} + +/// Checks that `expect_public` is the public key of `suri`. +/// +/// If `suri` has any derivations, `expect_public` is checked against the public key of the "bare" +/// `suri`, i.e. without any derivations. +/// +/// Returns an error if the public key does not match. +fn expect_public_from_phrase( + expect_public: &str, + suri: &str, + password: Option<&SecretString>, +) -> Result<(), Error> { + let secret_uri = SecretUri::from_str(suri).map_err(|e| format!("{:?}", e))?; + let expected_public = if let Some(public) = expect_public.strip_prefix("0x") { + let hex_public = array_bytes::hex2bytes(public) + .map_err(|_| format!("Invalid expected public key hex: `{}`", expect_public))?; + Pair::Public::try_from(&hex_public) + .map_err(|_| format!("Invalid expected public key: `{}`", expect_public))? + } else { + Pair::Public::from_string_with_version(expect_public) + .map_err(|_| format!("Invalid expected account id: `{}`", expect_public))? + .0 + }; + + let pair = Pair::from_string_with_seed( + secret_uri.phrase.expose_secret().as_str(), + password + .or(secret_uri.password.as_ref()) + .map(|p| p.expose_secret().as_str()), + ) + .map_err(|_| format!("Invalid secret uri: {}", suri))? + .0; + + if pair.public() == expected_public { + Ok(()) + } else { + Err(format!("Expected public ({}) key does not match.", expect_public).into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use sp_core::crypto::{ByteArray, Pair}; + use sp_runtime::traits::IdentifyAccount; + + #[test] + fn inspect() { + let words = + "remember fiber forum demise paper uniform squirrel feel access exclude casual effort"; + let seed = "0xad1fb77243b536b90cfe5f0d351ab1b1ac40e3890b41dc64f766ee56340cfca5"; + + let inspect = InspectKeyCmd::parse_from(["inspect-key", words, "--password", "12345"]); + assert!(inspect.run().is_ok()); + + let inspect = InspectKeyCmd::parse_from(["inspect-key", seed]); + assert!(inspect.run().is_ok()); + } + + #[test] + fn inspect_public_key() { + let public = "0x12e76e0ae8ce41b6516cce52b3f23a08dcb4cfeed53c6ee8f5eb9f7367341069"; + + let inspect = InspectKeyCmd::parse_from(["inspect-key", "--public", public]); + assert!(inspect.run().is_ok()); + } + + #[test] + fn inspect_with_expected_public_key() { + let check_cmd = |seed, expected_public, success| { + let inspect = InspectKeyCmd::parse_from([ + "inspect-key", + "--expect-public", + expected_public, + seed, + ]); + let res = inspect.run(); + + if success { + assert!(res.is_ok()); + } else { + assert!(res.unwrap_err().to_string().contains(&format!( + "Expected public ({}) key does not match.", + expected_public + ))); + } + }; + + let seed = + "remember fiber forum demise paper uniform squirrel feel access exclude casual effort"; + let invalid_public = "0x12e76e0ae8ce41b6516cce52b3f23a08dcb4cfeed53c6ee8f5eb9f7367341069"; + let valid_public = sp_core::sr25519::Pair::from_string_with_seed(seed, None) + .expect("Valid") + .0 + .public(); + let valid_public_hex = array_bytes::bytes2hex("0x", valid_public.as_slice()); + let valid_accountid = format!("{}", valid_public.into_account()); + + // It should fail with the invalid public key + check_cmd(seed, invalid_public, false); + + // It should work with the valid public key & account id + check_cmd(seed, &valid_public_hex, true); + check_cmd(seed, &valid_accountid, true); + + let password = "test12245"; + let seed_with_password = format!("{}///{}", seed, password); + let valid_public_with_password = + sp_core::sr25519::Pair::from_string_with_seed(&seed_with_password, Some(password)) + .expect("Valid") + .0 + .public(); + let valid_public_hex_with_password = + array_bytes::bytes2hex("0x", valid_public_with_password.as_slice()); + let valid_accountid_with_password = + format!("{}", valid_public_with_password.into_account()); + + // Only the public key that corresponds to the seed with password should be accepted. + check_cmd(&seed_with_password, &valid_public_hex, false); + check_cmd(&seed_with_password, &valid_accountid, false); + + check_cmd(&seed_with_password, &valid_public_hex_with_password, true); + check_cmd(&seed_with_password, &valid_accountid_with_password, true); + + let seed_with_password_and_derivation = format!("{}//test//account///{}", seed, password); + + let valid_public_with_password_and_derivation = + sp_core::sr25519::Pair::from_string_with_seed( + &seed_with_password_and_derivation, + Some(password), + ) + .expect("Valid") + .0 + .public(); + let valid_public_hex_with_password_and_derivation = + array_bytes::bytes2hex("0x", valid_public_with_password_and_derivation.as_slice()); + + // They should still be valid, because we check the base secret key. + check_cmd( + &seed_with_password_and_derivation, + &valid_public_hex_with_password, + true, + ); + check_cmd( + &seed_with_password_and_derivation, + &valid_accountid_with_password, + true, + ); + + // And these should be invalid. + check_cmd(&seed_with_password_and_derivation, &valid_public_hex, false); + check_cmd(&seed_with_password_and_derivation, &valid_accountid, false); + + // The public of the derived account should fail. + check_cmd( + &seed_with_password_and_derivation, + &valid_public_hex_with_password_and_derivation, + false, + ); + } +} diff --git a/substrate/cli/src/commands/inspect_node_key.rs b/substrate/cli/src/commands/inspect_node_key.rs new file mode 100644 index 00000000000..64beb7fc924 --- /dev/null +++ b/substrate/cli/src/commands/inspect_node_key.rs @@ -0,0 +1,92 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Implementation of the `inspect-node-key` subcommand + +use crate::Error; +use clap::Parser; +use libp2p_identity::Keypair; +use std::{ + fs, + io::{self, Read}, + path::PathBuf, +}; + +/// The `inspect-node-key` command +#[derive(Debug, Parser)] +#[command( + name = "inspect-node-key", + about = "Load a node key from a file or stdin and print the corresponding peer-id." +)] +pub struct InspectNodeKeyCmd { + /// Name of file to read the secret key from. + /// If not given, the secret key is read from stdin (up to EOF). + #[arg(long)] + file: Option, + + /// The input is in raw binary format. + /// If not given, the input is read as an hex encoded string. + #[arg(long)] + bin: bool, + + /// This argument is deprecated and has no effect for this command. + #[deprecated(note = "Network identifier is not used for node-key inspection")] + #[arg( + short = 'n', + long = "network", + value_name = "NETWORK", + ignore_case = true + )] + pub network_scheme: Option, +} + +impl InspectNodeKeyCmd { + /// runs the command + pub fn run(&self) -> Result<(), Error> { + let mut file_data = match &self.file { + Some(file) => fs::read(file)?, + None => { + let mut buf = Vec::with_capacity(64); + io::stdin().lock().read_to_end(&mut buf)?; + buf + } + }; + + if !self.bin { + // With hex input, give to the user a bit of tolerance about whitespaces + let keyhex = String::from_utf8_lossy(&file_data); + file_data = array_bytes::hex2bytes(keyhex.trim()) + .map_err(|_| "failed to decode secret as hex")?; + } + + let keypair = + Keypair::ed25519_from_bytes(&mut file_data).map_err(|_| "Bad node key file")?; + + println!("{}", keypair.public().to_peer_id()); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::commands::generate_node_key::GenerateNodeKeyCmd; + + use super::*; + + #[test] + fn inspect_node_key() { + let path = tempfile::tempdir() + .unwrap() + .keep() + .join("node-id") + .into_os_string(); + let path = path.to_str().unwrap(); + let cmd = GenerateNodeKeyCmd::parse_from(["generate-node-key", "--file", path]); + + assert!(cmd.run("test", &String::from("test")).is_ok()); + + let cmd = InspectNodeKeyCmd::parse_from(["inspect-node-key", "--file", path]); + assert!(cmd.run().is_ok()); + } +} diff --git a/substrate/cli/src/commands/key.rs b/substrate/cli/src/commands/key.rs new file mode 100644 index 00000000000..3701461d76d --- /dev/null +++ b/substrate/cli/src/commands/key.rs @@ -0,0 +1,46 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Key related CLI utilities + +use super::{ + generate::GenerateCmd, generate_node_key::GenerateNodeKeyCmd, insert_key::InsertKeyCmd, + inspect_key::InspectKeyCmd, inspect_node_key::InspectNodeKeyCmd, +}; +use crate::{Error, SubstrateCli}; + +/// Key utilities for the cli. +#[derive(Debug, clap::Subcommand)] +pub enum KeySubcommand { + /// Generate a random node key, write it to a file or stdout and write the + /// corresponding peer-id to stderr + GenerateNodeKey(GenerateNodeKeyCmd), + + /// Generate a random account + Generate(GenerateCmd), + + /// Gets a public key and a SS58 address from the provided Secret URI + Inspect(InspectKeyCmd), + + /// Load a node key from a file or stdin and print the corresponding peer-id + InspectNodeKey(InspectNodeKeyCmd), + + /// Insert a key to the keystore of a node. + Insert(InsertKeyCmd), +} + +impl KeySubcommand { + /// run the key subcommands + pub fn run(&self, cli: &C) -> Result<(), Error> { + match self { + KeySubcommand::GenerateNodeKey(cmd) => { + let chain_spec = cli.load_spec(cmd.chain.as_deref().unwrap_or(""))?; + cmd.run(chain_spec.id(), &C::executable_name()) + } + KeySubcommand::Generate(cmd) => cmd.run(), + KeySubcommand::Inspect(cmd) => cmd.run(), + KeySubcommand::Insert(cmd) => cmd.run(cli), + KeySubcommand::InspectNodeKey(cmd) => cmd.run(), + } + } +} diff --git a/substrate/cli/src/commands/mod.rs b/substrate/cli/src/commands/mod.rs new file mode 100644 index 00000000000..2e7dd139bd2 --- /dev/null +++ b/substrate/cli/src/commands/mod.rs @@ -0,0 +1,34 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Various subcommands that can be included in a substrate-based chain's CLI. + +mod build_spec_cmd; +mod chain_info_cmd; +mod check_block_cmd; +mod export_blocks_cmd; +mod export_state_cmd; +mod generate; +mod generate_node_key; +mod import_blocks_cmd; +mod insert_key; +mod inspect_key; +mod inspect_node_key; +mod key; +mod purge_chain_cmd; +mod revert_cmd; +mod run_cmd; +mod sign; +mod test; +pub mod utils; +mod vanity; +mod verify; + +pub use self::{ + build_spec_cmd::BuildSpecCmd, chain_info_cmd::ChainInfoCmd, check_block_cmd::CheckBlockCmd, + export_blocks_cmd::ExportBlocksCmd, export_state_cmd::ExportStateCmd, generate::GenerateCmd, + generate_node_key::GenerateKeyCmdCommon, import_blocks_cmd::ImportBlocksCmd, + insert_key::InsertKeyCmd, inspect_key::InspectKeyCmd, inspect_node_key::InspectNodeKeyCmd, + key::KeySubcommand, purge_chain_cmd::PurgeChainCmd, revert_cmd::RevertCmd, run_cmd::RunCmd, + sign::SignCmd, vanity::VanityCmd, verify::VerifyCmd, +}; diff --git a/substrate/cli/src/commands/purge_chain_cmd.rs b/substrate/cli/src/commands/purge_chain_cmd.rs new file mode 100644 index 00000000000..6d094aba1c5 --- /dev/null +++ b/substrate/cli/src/commands/purge_chain_cmd.rs @@ -0,0 +1,82 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::{ + error, + params::{DatabaseParams, SharedParams}, + CliConfiguration, +}; +use clap::Parser; +use sc_service::DatabaseSource; +use std::{ + fmt::Debug, + fs, + io::{self, Write}, +}; + +/// The `purge-chain` command used to remove the whole chain. +#[derive(Debug, Clone, Parser)] +pub struct PurgeChainCmd { + /// Skip interactive prompt by answering yes automatically. + #[arg(short = 'y')] + pub yes: bool, + + #[allow(missing_docs)] + #[clap(flatten)] + pub shared_params: SharedParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub database_params: DatabaseParams, +} + +impl PurgeChainCmd { + /// Run the purge command + pub fn run(&self, database_config: DatabaseSource) -> error::Result<()> { + let db_path = database_config + .path() + .and_then(|p| p.parent()) + .ok_or_else(|| { + error::Error::Input("Cannot purge custom database implementation".into()) + })?; + + if !self.yes { + print!("Are you sure to remove {:?}? [y/N]: ", db_path); + io::stdout().flush().expect("failed to flush stdout"); + + let mut input = String::new(); + io::stdin().read_line(&mut input)?; + let input = input.trim(); + + match input.chars().next() { + Some('y') | Some('Y') => {} + _ => { + println!("Aborted"); + return Ok(()); + } + } + } + + match fs::remove_dir_all(db_path) { + Ok(_) => { + println!("{:?} removed.", db_path); + Ok(()) + } + Err(ref err) if err.kind() == io::ErrorKind::NotFound => { + eprintln!("{:?} did not exist.", db_path); + Ok(()) + } + Err(err) => Result::Err(err.into()), + } + } +} + +impl CliConfiguration for PurgeChainCmd { + fn shared_params(&self) -> &SharedParams { + &self.shared_params + } + + fn database_params(&self) -> Option<&DatabaseParams> { + Some(&self.database_params) + } +} diff --git a/substrate/cli/src/commands/revert_cmd.rs b/substrate/cli/src/commands/revert_cmd.rs new file mode 100644 index 00000000000..89380370fa4 --- /dev/null +++ b/substrate/cli/src/commands/revert_cmd.rs @@ -0,0 +1,75 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::{ + error, + params::{DatabaseParams, GenericNumber, PruningParams, SharedParams}, + CliConfiguration, +}; +use clap::Parser; +use sc_client_api::{Backend, UsageProvider}; +use sc_service::chain_ops::revert_chain; +use sp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor}; +use std::{fmt::Debug, str::FromStr, sync::Arc}; + +/// The `revert` command used revert the chain to a previous state. +#[derive(Debug, Parser)] +pub struct RevertCmd { + /// Number of blocks to revert. + #[arg(default_value = "256")] + pub num: GenericNumber, + + #[allow(missing_docs)] + #[clap(flatten)] + pub shared_params: SharedParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub pruning_params: PruningParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub database_params: DatabaseParams, +} + +/// Revert handler for auxiliary data (e.g. consensus). +type AuxRevertHandler = + Box, Arc, NumberFor) -> error::Result<()>>; + +impl RevertCmd { + /// Run the revert command + pub async fn run( + &self, + client: Arc, + backend: Arc, + aux_revert: Option>, + ) -> error::Result<()> + where + B: BlockT, + BA: Backend, + C: UsageProvider, + <<::Header as HeaderT>::Number as FromStr>::Err: Debug, + { + let blocks = self.num.parse()?; + if let Some(aux_revert) = aux_revert { + aux_revert(client.clone(), backend.clone(), blocks)?; + } + revert_chain(client, backend, blocks)?; + + Ok(()) + } +} + +impl CliConfiguration for RevertCmd { + fn shared_params(&self) -> &SharedParams { + &self.shared_params + } + + fn pruning_params(&self) -> Option<&PruningParams> { + Some(&self.pruning_params) + } + + fn database_params(&self) -> Option<&DatabaseParams> { + Some(&self.database_params) + } +} diff --git a/substrate/cli/src/commands/run_cmd.rs b/substrate/cli/src/commands/run_cmd.rs new file mode 100644 index 00000000000..9a3e251e916 --- /dev/null +++ b/substrate/cli/src/commands/run_cmd.rs @@ -0,0 +1,738 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::{ + arg_enums::{Cors, RpcMethods}, + error::{Error, Result}, + params::{ + ImportParams, KeystoreParams, NetworkParams, OffchainWorkerParams, RpcEndpoint, + SharedParams, TransactionPoolParams, + }, + CliConfiguration, PrometheusParams, RuntimeParams, TelemetryParams, + RPC_DEFAULT_MAX_CONNECTIONS, RPC_DEFAULT_MAX_REQUEST_SIZE_MB, RPC_DEFAULT_MAX_RESPONSE_SIZE_MB, + RPC_DEFAULT_MAX_SUBS_PER_CONN, RPC_DEFAULT_MESSAGE_CAPACITY_PER_CONN, +}; +use clap::Parser; +use regex::Regex; +use sc_service::{ + config::{ + BasePath, IpNetwork, PrometheusConfig, RpcBatchRequestConfig, RpcMethodLimit, + TransactionPoolOptions, + }, + ChainSpec, Role, +}; +use sc_telemetry::TelemetryEndpoints; +use std::{ + net::{Ipv4Addr, Ipv6Addr, SocketAddr}, + num::NonZeroU32, +}; + +/// The `run` command used to run a node. +#[derive(Debug, Clone, Parser)] +pub struct RunCmd { + /// Enable validator mode. + /// + /// The node will be started with the authority role and actively + /// participate in any consensus task that it can (e.g. depending on + /// availability of local keys). + #[arg(long)] + pub validator: bool, + + /// Disable GRANDPA. + /// + /// Disables voter when running in validator mode, otherwise disable the GRANDPA + /// observer. + #[arg(long)] + pub no_grandpa: bool, + + /// Listen to all RPC interfaces (default: local). + /// + /// Not all RPC methods are safe to be exposed publicly. + /// + /// Use an RPC proxy server to filter out dangerous methods. More details: + /// . + /// + /// Use `--unsafe-rpc-external` to suppress the warning if you understand the risks. + #[arg(long)] + pub rpc_external: bool, + + /// Listen to all RPC interfaces. + /// + /// Same as `--rpc-external`. + #[arg(long)] + pub unsafe_rpc_external: bool, + + /// RPC methods to expose. + #[arg( + long, + value_name = "METHOD SET", + value_enum, + ignore_case = true, + default_value_t = RpcMethods::Auto, + verbatim_doc_comment + )] + pub rpc_methods: RpcMethods, + + /// RPC rate limiting (calls/minute) for each connection. + /// + /// This is disabled by default. + /// + /// For example `--rpc-rate-limit 10` will maximum allow + /// 10 calls per minute per connection. + #[arg(long)] + pub rpc_rate_limit: Option, + + /// Node-wide per-method RPC rate and concurrency budgets. + /// + /// Aliases share one budget across all HTTP and WebSocket connections. + /// Over-budget calls are rejected immediately. Unlike `--rpc-rate-limit`, this + /// budget is node-wide rather than per connection. + #[arg(long, value_name = "METHOD[,ALIAS...]=CALLS_PER_MINUTE,MAX_IN_FLIGHT")] + pub rpc_method_limit: Vec, + + /// Disable RPC rate limiting for certain ip addresses. + /// + /// Each IP address must be in CIDR notation such as `1.2.3.4/24`. + #[arg(long, num_args = 1..)] + pub rpc_rate_limit_whitelisted_ips: Vec, + + /// Trust proxy headers for disable rate limiting. + /// + /// By default the rpc server will not trust headers such `X-Real-IP`, `X-Forwarded-For` and + /// `Forwarded` and this option will make the rpc server to trust these headers. + /// + /// For instance this may be secure if the rpc server is behind a reverse proxy and that the + /// proxy always sets these headers. + #[arg(long)] + pub rpc_rate_limit_trust_proxy_headers: bool, + + /// Set the maximum RPC request payload size for both HTTP and WS in megabytes. + #[arg(long, default_value_t = RPC_DEFAULT_MAX_REQUEST_SIZE_MB)] + pub rpc_max_request_size: u32, + + /// Set the maximum RPC response payload size for both HTTP and WS in megabytes. + #[arg(long, default_value_t = RPC_DEFAULT_MAX_RESPONSE_SIZE_MB)] + pub rpc_max_response_size: u32, + + /// Set the maximum concurrent subscriptions per connection. + #[arg(long, default_value_t = RPC_DEFAULT_MAX_SUBS_PER_CONN)] + pub rpc_max_subscriptions_per_connection: u32, + + /// Specify JSON-RPC server TCP port. + #[arg(long, value_name = "PORT")] + pub rpc_port: Option, + + /// EXPERIMENTAL: Specify the JSON-RPC server interface and this option which can be enabled + /// several times if you want expose several RPC interfaces with different configurations. + /// + /// The format for this option is: + /// `--experimental-rpc-endpoint" listen-addr=,,..."` where each option is + /// separated by a comma and `listen-addr` is the only required param. + /// + /// The following options are available: + /// • listen-addr: The socket address (ip:port) to listen on. Be careful to not expose the + /// server to the public internet unless you know what you're doing. (required) + /// • disable-batch-requests: Disable batch requests (optional) + /// • max-connections: The maximum number of concurrent connections that the server will + /// accept (optional) + /// • max-request-size: The maximum size of a request body in megabytes (optional) + /// • max-response-size: The maximum size of a response body in megabytes (optional) + /// • max-subscriptions-per-connection: The maximum number of subscriptions per connection + /// (optional) + /// • max-buffer-capacity-per-connection: The maximum buffer capacity per connection + /// (optional) + /// • max-batch-request-len: The maximum number of requests in a batch (optional) + /// • cors: The CORS allowed origins, this can enabled more than once (optional) + /// • methods: Which RPC methods to allow, valid values are "safe", "unsafe" and "auto" + /// (optional) + /// • optional: If the listen address is optional i.e the interface is not required to be + /// available For example this may be useful if some platforms doesn't support ipv6 + /// (optional) + /// • rate-limit: The rate limit in calls per minute for each connection (optional) + /// • rate-limit-trust-proxy-headers: Trust proxy headers for disable rate limiting (optional) + /// • rate-limit-whitelisted-ips: Disable rate limiting for certain ip addresses, this can be + /// enabled more than once (optional) • retry-random-port: If the port is already in use, + /// retry with a random port (optional) + /// + /// Use with care, this flag is unstable and subject to change. + #[arg( + long, + num_args = 1.., + verbatim_doc_comment, + conflicts_with_all = &["rpc_external", "unsafe_rpc_external", "rpc_port", "rpc_cors", "rpc_rate_limit_trust_proxy_headers", "rpc_rate_limit", "rpc_rate_limit_whitelisted_ips", "rpc_message_buffer_capacity_per_connection", "rpc_disable_batch_requests", "rpc_max_subscriptions_per_connection", "rpc_max_request_size", "rpc_max_response_size"] + )] + pub experimental_rpc_endpoint: Vec, + + /// Maximum number of RPC server connections. + #[arg(long, value_name = "COUNT", default_value_t = RPC_DEFAULT_MAX_CONNECTIONS)] + pub rpc_max_connections: u32, + + /// The number of messages the RPC server is allowed to keep in memory. + /// + /// If the buffer becomes full then the server will not process + /// new messages until the connected client start reading the + /// underlying messages. + /// + /// This applies per connection which includes both + /// JSON-RPC methods calls and subscriptions. + #[arg(long, default_value_t = RPC_DEFAULT_MESSAGE_CAPACITY_PER_CONN)] + pub rpc_message_buffer_capacity_per_connection: u32, + + /// Disable RPC batch requests + #[arg(long, alias = "rpc_no_batch_requests", conflicts_with_all = &["rpc_max_batch_request_len"])] + pub rpc_disable_batch_requests: bool, + + /// Limit the max length per RPC batch request + #[arg(long, conflicts_with_all = &["rpc_disable_batch_requests"], value_name = "LEN")] + pub rpc_max_batch_request_len: Option, + + /// Specify browser *origins* allowed to access the HTTP & WS RPC servers. + /// + /// A comma-separated list of origins (protocol://domain or special `null` + /// value). Value of `all` will disable origin validation. Default is to + /// allow localhost and origins. When running in + /// `--dev` mode the default is to allow all origins. + #[arg(long, value_name = "ORIGINS")] + pub rpc_cors: Option, + + /// The human-readable name for this node. + /// + /// It's used as network node name. + #[arg(long, value_name = "NAME")] + pub name: Option, + + #[allow(missing_docs)] + #[clap(flatten)] + pub telemetry_params: TelemetryParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub prometheus_params: PrometheusParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub runtime_params: RuntimeParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub offchain_worker_params: OffchainWorkerParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub shared_params: SharedParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub import_params: ImportParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub network_params: NetworkParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub pool_config: TransactionPoolParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub keystore_params: KeystoreParams, + + /// Shortcut for `--name Alice --validator`. + /// + /// Session keys for `Alice` are added to keystore. + #[arg(long, conflicts_with_all = &["bob", "charlie", "dave", "eve", "ferdie", "one", "two"])] + pub alice: bool, + + /// Shortcut for `--name Bob --validator`. + /// + /// Session keys for `Bob` are added to keystore. + #[arg(long, conflicts_with_all = &["alice", "charlie", "dave", "eve", "ferdie", "one", "two"])] + pub bob: bool, + + /// Shortcut for `--name Charlie --validator`. + /// + /// Session keys for `Charlie` are added to keystore. + #[arg(long, conflicts_with_all = &["alice", "bob", "dave", "eve", "ferdie", "one", "two"])] + pub charlie: bool, + + /// Shortcut for `--name Dave --validator`. + /// + /// Session keys for `Dave` are added to keystore. + #[arg(long, conflicts_with_all = &["alice", "bob", "charlie", "eve", "ferdie", "one", "two"])] + pub dave: bool, + + /// Shortcut for `--name Eve --validator`. + /// + /// Session keys for `Eve` are added to keystore. + #[arg(long, conflicts_with_all = &["alice", "bob", "charlie", "dave", "ferdie", "one", "two"])] + pub eve: bool, + + /// Shortcut for `--name Ferdie --validator`. + /// + /// Session keys for `Ferdie` are added to keystore. + #[arg(long, conflicts_with_all = &["alice", "bob", "charlie", "dave", "eve", "one", "two"])] + pub ferdie: bool, + + /// Shortcut for `--name One --validator`. + /// + /// Session keys for `One` are added to keystore. + #[arg(long, conflicts_with_all = &["alice", "bob", "charlie", "dave", "eve", "ferdie", "two"])] + pub one: bool, + + /// Shortcut for `--name Two --validator`. + /// + /// Session keys for `Two` are added to keystore. + #[arg(long, conflicts_with_all = &["alice", "bob", "charlie", "dave", "eve", "ferdie", "one"])] + pub two: bool, + + /// Enable authoring even when offline. + #[arg(long)] + pub force_authoring: bool, + + /// Run a temporary node. + /// + /// A temporary directory will be created to store the configuration and will be deleted + /// at the end of the process. + /// + /// Note: the directory is random per process execution. This directory is used as base path + /// which includes: database, node key and keystore. + /// + /// When `--dev` is given and no explicit `--base-path`, this option is implied. + #[arg(long, conflicts_with = "base_path")] + pub tmp: bool, +} + +impl RunCmd { + /// Get the `Sr25519Keyring` matching one of the flag. + pub fn get_keyring(&self) -> Option { + use sp_keyring::Sr25519Keyring::*; + + if self.alice { + Some(Alice) + } else if self.bob { + Some(Bob) + } else if self.charlie { + Some(Charlie) + } else if self.dave { + Some(Dave) + } else if self.eve { + Some(Eve) + } else if self.ferdie { + Some(Ferdie) + } else if self.one { + Some(One) + } else if self.two { + Some(Two) + } else { + None + } + } +} + +impl CliConfiguration for RunCmd { + fn shared_params(&self) -> &SharedParams { + &self.shared_params + } + + fn import_params(&self) -> Option<&ImportParams> { + Some(&self.import_params) + } + + fn network_params(&self) -> Option<&NetworkParams> { + let network_params = &self.network_params; + let is_authority = self.role(self.is_dev().ok()?).ok()?.is_authority(); + if is_authority && network_params.public_addr.is_empty() { + eprintln!( + "WARNING: No public address specified, validator node may not be reachable. + Consider setting `--public-addr` to the public IP address of this node. + This will become a hard requirement in future versions." + ); + } + + Some(network_params) + } + + fn keystore_params(&self) -> Option<&KeystoreParams> { + Some(&self.keystore_params) + } + + fn offchain_worker_params(&self) -> Option<&OffchainWorkerParams> { + Some(&self.offchain_worker_params) + } + + fn node_name(&self) -> Result { + let name: String = match (self.name.as_ref(), self.get_keyring()) { + (Some(name), _) => name.to_string(), + (_, Some(keyring)) => keyring.to_string(), + (None, None) => crate::generate_node_name(), + }; + + is_node_name_valid(&name).map_err(|msg| { + Error::Input(format!( + "Invalid node name '{}'. Reason: {}. If unsure, use none.", + name, msg + )) + })?; + + Ok(name) + } + + fn dev_key_seed(&self, is_dev: bool) -> Result> { + Ok(self.get_keyring().map(|a| format!("//{}", a)).or_else(|| { + if is_dev { + Some("//Alice".into()) + } else { + None + } + })) + } + + fn telemetry_endpoints( + &self, + chain_spec: &Box, + ) -> Result> { + let params = &self.telemetry_params; + Ok(if params.no_telemetry { + None + } else if !params.telemetry_endpoints.is_empty() { + Some( + TelemetryEndpoints::new(params.telemetry_endpoints.clone()) + .map_err(|e| e.to_string())?, + ) + } else { + chain_spec.telemetry_endpoints().clone() + }) + } + + fn role(&self, is_dev: bool) -> Result { + let keyring = self.get_keyring(); + let is_authority = self.validator || is_dev || keyring.is_some(); + + Ok(if is_authority { + Role::Authority + } else { + Role::Full + }) + } + + fn force_authoring(&self) -> Result { + // Imply forced authoring on --dev + Ok(self.shared_params.dev || self.force_authoring) + } + + fn prometheus_config( + &self, + default_listen_port: u16, + chain_spec: &Box, + ) -> Result> { + Ok(self + .prometheus_params + .prometheus_config(default_listen_port, chain_spec.id().to_string())) + } + + fn disable_grandpa(&self) -> Result { + Ok(self.no_grandpa) + } + + fn rpc_max_connections(&self) -> Result { + Ok(self.rpc_max_connections) + } + + fn rpc_cors(&self, is_dev: bool) -> Result>> { + Ok(self + .rpc_cors + .clone() + .unwrap_or_else(|| { + if is_dev { + log::warn!("Running in --dev mode, RPC CORS has been disabled."); + Cors::All + } else { + Cors::List(vec![ + "http://localhost:*".into(), + "http://127.0.0.1:*".into(), + "https://localhost:*".into(), + "https://127.0.0.1:*".into(), + "https://polkadot.js.org".into(), + ]) + } + }) + .into()) + } + + fn rpc_addr(&self, default_listen_port: u16) -> Result>> { + if !self.experimental_rpc_endpoint.is_empty() { + for endpoint in &self.experimental_rpc_endpoint { + // Technically, `0.0.0.0` isn't a public IP address, but it's a way to listen on + // all interfaces. Thus, we consider it as a public endpoint and warn about it. + if endpoint.rpc_methods == RpcMethods::Unsafe && endpoint.is_global() + || endpoint.listen_addr.ip().is_unspecified() + { + log::warn!( + "It isn't safe to expose RPC publicly without a proxy server that filters \ + available set of RPC methods." + ); + } + } + + return Ok(Some(self.experimental_rpc_endpoint.clone())); + } + + let (ipv4, ipv6) = rpc_interface( + self.rpc_external, + self.unsafe_rpc_external, + self.rpc_methods, + self.validator, + )?; + + let cors = self.rpc_cors(self.is_dev()?)?; + let port = self.rpc_port.unwrap_or(default_listen_port); + + Ok(Some(vec![ + RpcEndpoint { + batch_config: self.rpc_batch_config()?, + max_connections: self.rpc_max_connections, + listen_addr: SocketAddr::new(std::net::IpAddr::V4(ipv4), port), + rpc_methods: self.rpc_methods, + rate_limit: self.rpc_rate_limit, + rate_limit_trust_proxy_headers: self.rpc_rate_limit_trust_proxy_headers, + rate_limit_whitelisted_ips: self.rpc_rate_limit_whitelisted_ips.clone(), + max_payload_in_mb: self.rpc_max_request_size, + max_payload_out_mb: self.rpc_max_response_size, + max_subscriptions_per_connection: self.rpc_max_subscriptions_per_connection, + max_buffer_capacity_per_connection: self.rpc_message_buffer_capacity_per_connection, + cors: cors.clone(), + retry_random_port: true, + is_optional: false, + }, + RpcEndpoint { + batch_config: self.rpc_batch_config()?, + max_connections: self.rpc_max_connections, + listen_addr: SocketAddr::new(std::net::IpAddr::V6(ipv6), port), + rpc_methods: self.rpc_methods, + rate_limit: self.rpc_rate_limit, + rate_limit_trust_proxy_headers: self.rpc_rate_limit_trust_proxy_headers, + rate_limit_whitelisted_ips: self.rpc_rate_limit_whitelisted_ips.clone(), + max_payload_in_mb: self.rpc_max_request_size, + max_payload_out_mb: self.rpc_max_response_size, + max_subscriptions_per_connection: self.rpc_max_subscriptions_per_connection, + max_buffer_capacity_per_connection: self.rpc_message_buffer_capacity_per_connection, + cors: cors.clone(), + retry_random_port: true, + is_optional: true, + }, + ])) + } + + fn rpc_methods(&self) -> Result { + Ok(self.rpc_methods.into()) + } + + fn rpc_max_request_size(&self) -> Result { + Ok(self.rpc_max_request_size) + } + + fn rpc_max_response_size(&self) -> Result { + Ok(self.rpc_max_response_size) + } + + fn rpc_max_subscriptions_per_connection(&self) -> Result { + Ok(self.rpc_max_subscriptions_per_connection) + } + + fn rpc_buffer_capacity_per_connection(&self) -> Result { + Ok(self.rpc_message_buffer_capacity_per_connection) + } + + fn rpc_batch_config(&self) -> Result { + let cfg = if self.rpc_disable_batch_requests { + RpcBatchRequestConfig::Disabled + } else if let Some(l) = self.rpc_max_batch_request_len { + RpcBatchRequestConfig::Limit(l) + } else { + RpcBatchRequestConfig::Unlimited + }; + + Ok(cfg) + } + + fn rpc_rate_limit(&self) -> Result> { + Ok(self.rpc_rate_limit) + } + + fn rpc_method_limits(&self) -> Result> { + RpcMethodLimit::validate_all(&self.rpc_method_limit).map_err(Error::Input)?; + Ok(self.rpc_method_limit.clone()) + } + + fn rpc_rate_limit_whitelisted_ips(&self) -> Result> { + Ok(self.rpc_rate_limit_whitelisted_ips.clone()) + } + + fn rpc_rate_limit_trust_proxy_headers(&self) -> Result { + Ok(self.rpc_rate_limit_trust_proxy_headers) + } + + fn transaction_pool(&self, is_dev: bool) -> Result { + Ok(self.pool_config.transaction_pool(is_dev)) + } + + fn max_runtime_instances(&self) -> Result> { + Ok(Some(self.runtime_params.max_runtime_instances)) + } + + fn runtime_cache_size(&self) -> Result { + Ok(self.runtime_params.runtime_cache_size) + } + + fn base_path(&self) -> Result> { + Ok(if self.tmp { + Some(BasePath::new_temp_dir()?) + } else { + match self.shared_params().base_path()? { + Some(r) => Some(r), + // If `dev` is enabled, we use the temp base path. + None if self.shared_params().is_dev() => Some(BasePath::new_temp_dir()?), + None => None, + } + }) + } +} + +/// Check whether a node name is considered as valid. +pub fn is_node_name_valid(_name: &str) -> std::result::Result<(), &str> { + let name = _name.to_string(); + + if name.is_empty() { + return Err("Node name cannot be empty"); + } + + if name.chars().count() >= crate::NODE_NAME_MAX_LENGTH { + return Err("Node name too long"); + } + + let invalid_chars = r"[\\.@]"; + let re = Regex::new(invalid_chars).unwrap(); + if re.is_match(&name) { + return Err("Node name should not contain invalid chars such as '.' and '@'"); + } + + let invalid_patterns = r"^https?:"; + let re = Regex::new(invalid_patterns).unwrap(); + if re.is_match(&name) { + return Err("Node name should not contain urls"); + } + + Ok(()) +} + +fn rpc_interface( + is_external: bool, + is_unsafe_external: bool, + rpc_methods: RpcMethods, + is_validator: bool, +) -> Result<(Ipv4Addr, Ipv6Addr)> { + if is_external && is_validator && rpc_methods != RpcMethods::Unsafe { + return Err(Error::Input( + "--rpc-external option shouldn't be used if the node is running as \ + a validator. Use `--unsafe-rpc-external` or `--rpc-methods=unsafe` if you understand \ + the risks. See the options description for more information." + .to_owned(), + )); + } + + if is_external || is_unsafe_external { + if rpc_methods == RpcMethods::Unsafe { + log::warn!( + "It isn't safe to expose RPC publicly without a proxy server that filters \ + available set of RPC methods." + ); + } + + Ok((Ipv4Addr::UNSPECIFIED, Ipv6Addr::UNSPECIFIED)) + } else { + Ok((Ipv4Addr::LOCALHOST, Ipv6Addr::LOCALHOST)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tests_node_name_good() { + assert!(is_node_name_valid("short name").is_ok()); + assert!(is_node_name_valid("www").is_ok()); + assert!(is_node_name_valid("aawww").is_ok()); + assert!(is_node_name_valid("wwwaa").is_ok()); + assert!(is_node_name_valid("www aa").is_ok()); + } + + #[test] + fn tests_node_name_bad() { + assert!(is_node_name_valid("").is_err()); + assert!(is_node_name_valid( + "very very long names are really not very cool for the ui at all, really they're not" + ) + .is_err()); + assert!(is_node_name_valid("Dots.not.Ok").is_err()); + // NOTE: the urls below don't include a domain otherwise + // they'd get filtered for including a `.` + assert!(is_node_name_valid("http://visitme").is_err()); + assert!(is_node_name_valid("http:/visitme").is_err()); + assert!(is_node_name_valid("http:visitme").is_err()); + assert!(is_node_name_valid("https://visitme").is_err()); + assert!(is_node_name_valid("https:/visitme").is_err()); + assert!(is_node_name_valid("https:visitme").is_err()); + assert!(is_node_name_valid("www.visit.me").is_err()); + assert!(is_node_name_valid("www.visit").is_err()); + assert!(is_node_name_valid("hello\\world").is_err()); + assert!(is_node_name_valid("visit.www").is_err()); + assert!(is_node_name_valid("email@domain").is_err()); + } + + #[test] + fn rpc_method_limits_parse_repeatedly() { + let cmd = RunCmd::try_parse_from([ + "run", + "--rpc-method-limit", + "state_getRuntimeVersion,chain_getRuntimeVersion=60,2", + "--rpc-method-limit", + "state_getMetadata=30,1", + ]) + .expect("valid method limits should parse"); + + assert_eq!(cmd.rpc_method_limit.len(), 2); + assert_eq!( + cmd.rpc_method_limits().expect("valid limits"), + cmd.rpc_method_limit + ); + } + + #[test] + fn rpc_method_limits_reject_duplicates() { + let cmd = RunCmd::try_parse_from([ + "run", + "--rpc-method-limit", + "state_getRuntimeVersion=60,2", + "--rpc-method-limit", + "state_getRuntimeVersion=30,1", + ]) + .expect("well-formed method limits should parse"); + + assert!(cmd.rpc_method_limits().is_err()); + } + + #[test] + fn rpc_method_limits_default_to_empty() { + let cmd = RunCmd::try_parse_from(["run"]).expect("default run command should parse"); + + assert!(cmd.rpc_method_limits().expect("default limits").is_empty()); + } + + #[test] + fn rpc_method_limits_reject_invalid_values() { + assert!( + RunCmd::try_parse_from(["run", "--rpc-method-limit", "state_getMetadata=0,1"]).is_err() + ); + } +} diff --git a/substrate/cli/src/commands/sign.rs b/substrate/cli/src/commands/sign.rs new file mode 100644 index 00000000000..1bf96e86be4 --- /dev/null +++ b/substrate/cli/src/commands/sign.rs @@ -0,0 +1,117 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Implementation of the `sign` subcommand +use crate::{ + error, params::MessageParams, utils, with_crypto_scheme, CryptoSchemeFlag, KeystoreParams, +}; +use array_bytes::bytes2hex; +use clap::Parser; +use sp_core::crypto::SecretString; +use std::io::{BufRead, Write}; + +/// The `sign` command +#[derive(Debug, Clone, Parser)] +#[command(name = "sign", about = "Sign a message, with a given (secret) key")] +pub struct SignCmd { + /// The secret key URI. + /// If the value is a file, the file content is used as URI. + /// If not given, you will be prompted for the URI. + #[arg(long)] + suri: Option, + + #[allow(missing_docs)] + #[clap(flatten)] + pub message_params: MessageParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub keystore_params: KeystoreParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub crypto_scheme: CryptoSchemeFlag, +} + +impl SignCmd { + /// Run the command + pub fn run(&self) -> error::Result<()> { + let sig = self.sign(|| std::io::stdin().lock())?; + std::io::stdout().lock().write_all(sig.as_bytes())?; + Ok(()) + } + + /// Sign a message. + /// + /// The message can either be provided as immediate argument via CLI or otherwise read from the + /// reader created by `create_reader`. The reader will only be created in case that the message + /// is not passed as immediate. + pub(crate) fn sign(&self, create_reader: F) -> error::Result + where + R: BufRead, + F: FnOnce() -> R, + { + let message = self.message_params.message_from(create_reader)?; + let suri = utils::read_uri(self.suri.as_ref())?; + let password = self.keystore_params.read_password()?; + + with_crypto_scheme!(self.crypto_scheme.scheme, sign(&suri, password, message)) + } +} + +fn sign( + suri: &str, + password: Option, + message: Vec, +) -> error::Result { + let pair = utils::pair_from_suri::

(suri, password)?; + Ok(bytes2hex("0x", pair.sign(&message).as_ref())) +} + +#[cfg(test)] +mod test { + use super::*; + + const SEED: &str = "0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a"; + + #[test] + fn sign_arg() { + let cmd = SignCmd::parse_from([ + "sign", + "--suri", + SEED, + "--message", + SEED, + "--password", + "12345", + "--hex", + ]); + let sig = cmd.sign(|| std::io::stdin().lock()).expect("Must sign"); + + assert!(sig.starts_with("0x"), "Signature must start with 0x"); + assert!( + array_bytes::hex2bytes(&sig).is_ok(), + "Signature is valid hex" + ); + } + + #[test] + fn sign_stdin() { + let cmd = SignCmd::parse_from([ + "sign", + "--suri", + SEED, + "--message", + SEED, + "--password", + "12345", + ]); + let sig = cmd.sign(|| SEED.as_bytes()).expect("Must sign"); + + assert!(sig.starts_with("0x"), "Signature must start with 0x"); + assert!( + array_bytes::hex2bytes(&sig).is_ok(), + "Signature is valid hex" + ); + } +} diff --git a/substrate/cli/src/commands/test/mod.rs b/substrate/cli/src/commands/test/mod.rs new file mode 100644 index 00000000000..2ec35c6f918 --- /dev/null +++ b/substrate/cli/src/commands/test/mod.rs @@ -0,0 +1,6 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Integration tests for subkey commands. + +mod sig_verify; diff --git a/substrate/cli/src/commands/test/sig_verify.rs b/substrate/cli/src/commands/test/sig_verify.rs new file mode 100644 index 00000000000..7d62282cffd --- /dev/null +++ b/substrate/cli/src/commands/test/sig_verify.rs @@ -0,0 +1,141 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +#![cfg(test)] + +//! Integration test that the `sign` and `verify` sub-commands work together. + +use crate::*; +use clap::Parser; + +const SEED: &str = "0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a"; +const ALICE: &str = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"; +const BOB: &str = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"; + +/// Sign a valid UFT-8 message which can be `hex` and passed either via `stdin` or as an argument. +fn sign(msg: &str, hex: bool, stdin: bool) -> String { + sign_raw(msg.as_bytes(), hex, stdin) +} + +/// Sign a raw message which can be `hex` and passed either via `stdin` or as an argument. +fn sign_raw(msg: &[u8], hex: bool, stdin: bool) -> String { + let mut args = vec!["sign", "--suri", SEED]; + if !stdin { + args.push("--message"); + args.push(std::str::from_utf8(msg).expect("Can only pass valid UTF-8 as arg")); + } + if hex { + args.push("--hex"); + } + let cmd = SignCmd::parse_from(&args); + cmd.sign(|| msg) + .expect("Static data is good; Must sign; qed") +} + +/// Verify a valid UFT-8 message which can be `hex` and passed either via `stdin` or as an argument. +fn verify(msg: &str, hex: bool, stdin: bool, who: &str, sig: &str) -> bool { + verify_raw(msg.as_bytes(), hex, stdin, who, sig) +} + +/// Verify a raw message which can be `hex` and passed either via `stdin` or as an argument. +fn verify_raw(msg: &[u8], hex: bool, stdin: bool, who: &str, sig: &str) -> bool { + let mut args = vec!["verify", sig, who]; + if !stdin { + args.push("--message"); + args.push(std::str::from_utf8(msg).expect("Can only pass valid UTF-8 as arg")); + } + if hex { + args.push("--hex"); + } + let cmd = VerifyCmd::parse_from(&args); + cmd.verify(|| msg).is_ok() +} + +/// Test that sig/verify works with UTF-8 bytes passed as arg. +#[test] +fn sig_verify_arg_utf8_work() { + let sig = sign("Something", false, false); + + assert!(verify("Something", false, false, ALICE, &sig)); + assert!(!verify("Something", false, false, BOB, &sig)); + + assert!(!verify("Wrong", false, false, ALICE, &sig)); + assert!(!verify("Not hex", true, false, ALICE, &sig)); + assert!(!verify("0x1234", true, false, ALICE, &sig)); + assert!(!verify("Wrong", false, false, BOB, &sig)); + assert!(!verify("Not hex", true, false, BOB, &sig)); + assert!(!verify("0x1234", true, false, BOB, &sig)); +} + +/// Test that sig/verify works with UTF-8 bytes passed via stdin. +#[test] +fn sig_verify_stdin_utf8_work() { + let sig = sign("Something", false, true); + + assert!(verify("Something", false, true, ALICE, &sig)); + assert!(!verify("Something", false, true, BOB, &sig)); + + assert!(!verify("Wrong", false, true, ALICE, &sig)); + assert!(!verify("Not hex", true, true, ALICE, &sig)); + assert!(!verify("0x1234", true, true, ALICE, &sig)); + assert!(!verify("Wrong", false, true, BOB, &sig)); + assert!(!verify("Not hex", true, true, BOB, &sig)); + assert!(!verify("0x1234", true, true, BOB, &sig)); +} + +/// Test that sig/verify works with hex bytes passed as arg. +#[test] +fn sig_verify_arg_hex_work() { + let sig = sign("0xaabbcc", true, false); + + assert!(verify("0xaabbcc", true, false, ALICE, &sig)); + assert!(verify("aabBcc", true, false, ALICE, &sig)); + assert!(verify("0xaAbbCC", true, false, ALICE, &sig)); + assert!(!verify("0xaabbcc", true, false, BOB, &sig)); + + assert!(!verify("0xaabbcc", false, false, ALICE, &sig)); +} + +/// Test that sig/verify works with hex bytes passed via stdin. +#[test] +fn sig_verify_stdin_hex_work() { + let sig = sign("0xaabbcc", true, true); + + assert!(verify("0xaabbcc", true, true, ALICE, &sig)); + assert!(verify("aabBcc", true, true, ALICE, &sig)); + assert!(verify("0xaAbbCC", true, true, ALICE, &sig)); + assert!(!verify("0xaabbcc", true, true, BOB, &sig)); + + assert!(!verify("0xaabbcc", false, true, ALICE, &sig)); +} + +/// Test that sig/verify works with random bytes. +#[test] +fn sig_verify_stdin_non_utf8_work() { + use rand::RngCore; + let mut rng = rand::thread_rng(); + + for _ in 0..100 { + let mut raw = [0u8; 32]; + rng.fill_bytes(&mut raw); + let sig = sign_raw(&raw, false, true); + + assert!(verify_raw(&raw, false, true, ALICE, &sig)); + assert!(!verify_raw(&raw, false, true, BOB, &sig)); + } +} + +/// Test that sig/verify works with invalid UTF-8 bytes. +#[test] +fn sig_verify_stdin_invalid_utf8_work() { + let raw = vec![192u8, 193]; + assert!( + String::from_utf8(raw.clone()).is_err(), + "Must be invalid UTF-8" + ); + + let sig = sign_raw(&raw, false, true); + + assert!(verify_raw(&raw, false, true, ALICE, &sig)); + assert!(!verify_raw(&raw, false, true, BOB, &sig)); +} diff --git a/substrate/cli/src/commands/utils.rs b/substrate/cli/src/commands/utils.rs new file mode 100644 index 00000000000..fe5b413bc83 --- /dev/null +++ b/substrate/cli/src/commands/utils.rs @@ -0,0 +1,302 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! subcommand utilities +use crate::{ + error::{self, Error}, + OutputType, +}; +use serde_json::json; +use sp_core::{ + crypto::{ + unwrap_or_default_ss58_version, ExposeSecret, SecretString, Ss58AddressFormat, Ss58Codec, + Zeroize, + }, + hexdisplay::HexDisplay, + Pair, +}; +use sp_runtime::{traits::IdentifyAccount, MultiSigner}; +use std::path::PathBuf; + +/// Public key type for Runtime +pub type PublicFor

=

::Public; +/// Seed type for Runtime +pub type SeedFor

=

::Seed; + +/// helper method to fetch uri from `Option` either as a file or read from stdin +pub fn read_uri(uri: Option<&String>) -> error::Result { + let uri = if let Some(uri) = uri { + let file = PathBuf::from(&uri); + if file.is_file() { + std::fs::read_to_string(uri)?.trim_end().to_owned() + } else { + uri.into() + } + } else { + rpassword::prompt_password("URI: ")? + }; + + Ok(uri) +} + +/// Try to parse given `uri` and print relevant information. +/// +/// 1. Try to construct the `Pair` while using `uri` as input for [`sp_core::Pair::from_phrase`]. +/// +/// 2. Try to construct the `Pair` while using `uri` as input for +/// [`sp_core::Pair::from_string_with_seed`]. +/// +/// 3. Try to construct the `Pair::Public` while using `uri` as input for +/// [`sp_core::crypto::Ss58Codec::from_string_with_version`]. +pub fn print_from_uri( + uri: &str, + password: Option, + network_override: Option, + output: OutputType, +) where + Pair: sp_core::Pair, + Pair::Public: Into, +{ + let password = password.as_ref().map(|s| s.expose_secret().as_str()); + let network_id = String::from(unwrap_or_default_ss58_version(network_override)); + if let Ok((pair, seed)) = Pair::from_phrase(uri, password) { + let public_key = pair.public(); + let network_override = unwrap_or_default_ss58_version(network_override); + + match output { + OutputType::Json => { + let json = json!({ + "secretPhrase": uri, + "networkId": network_id, + "secretSeed": format_seed::(seed), + "publicKey": format_public_key::(public_key.clone()), + "ss58PublicKey": public_key.to_ss58check_with_version(network_override), + "accountId": format_account_id::(public_key), + "ss58Address": pair.public().into().into_account().to_ss58check_with_version(network_override), + }); + println!( + "{}", + serde_json::to_string_pretty(&json).expect("Json pretty print failed") + ); + } + OutputType::Text => { + println!( + "Secret phrase: {}\n \ + Network ID: {}\n \ + Secret seed: {}\n \ + Public key (hex): {}\n \ + Account ID: {}\n \ + Public key (SS58): {}\n \ + SS58 Address: {}", + uri, + network_id, + format_seed::(seed), + format_public_key::(public_key.clone()), + format_account_id::(public_key.clone()), + public_key.to_ss58check_with_version(network_override), + pair.public() + .into() + .into_account() + .to_ss58check_with_version(network_override), + ); + } + } + } else if let Ok((pair, seed)) = Pair::from_string_with_seed(uri, password) { + let public_key = pair.public(); + let network_override = unwrap_or_default_ss58_version(network_override); + + match output { + OutputType::Json => { + let json = json!({ + "secretKeyUri": uri, + "networkId": network_id, + "secretSeed": if let Some(seed) = seed { format_seed::(seed) } else { "n/a".into() }, + "publicKey": format_public_key::(public_key.clone()), + "ss58PublicKey": public_key.to_ss58check_with_version(network_override), + "accountId": format_account_id::(public_key), + "ss58Address": pair.public().into().into_account().to_ss58check_with_version(network_override), + }); + println!( + "{}", + serde_json::to_string_pretty(&json).expect("Json pretty print failed") + ); + } + OutputType::Text => { + println!( + "Secret Key URI `{}` is account:\n \ + Network ID: {}\n \ + Secret seed: {}\n \ + Public key (hex): {}\n \ + Account ID: {}\n \ + Public key (SS58): {}\n \ + SS58 Address: {}", + uri, + network_id, + if let Some(seed) = seed { + format_seed::(seed) + } else { + "n/a".into() + }, + format_public_key::(public_key.clone()), + format_account_id::(public_key.clone()), + public_key.to_ss58check_with_version(network_override), + pair.public() + .into() + .into_account() + .to_ss58check_with_version(network_override), + ); + } + } + } else if let Ok((public_key, network)) = Pair::Public::from_string_with_version(uri) { + let network_override = network_override.unwrap_or(network); + + match output { + OutputType::Json => { + let json = json!({ + "publicKeyUri": uri, + "networkId": String::from(network_override), + "publicKey": format_public_key::(public_key.clone()), + "accountId": format_account_id::(public_key.clone()), + "ss58PublicKey": public_key.to_ss58check_with_version(network_override), + "ss58Address": public_key.to_ss58check_with_version(network_override), + }); + + println!( + "{}", + serde_json::to_string_pretty(&json).expect("Json pretty print failed") + ); + } + OutputType::Text => { + println!( + "Public Key URI `{}` is account:\n \ + Network ID/Version: {}\n \ + Public key (hex): {}\n \ + Account ID: {}\n \ + Public key (SS58): {}\n \ + SS58 Address: {}", + uri, + String::from(network_override), + format_public_key::(public_key.clone()), + format_account_id::(public_key.clone()), + public_key.to_ss58check_with_version(network_override), + public_key.to_ss58check_with_version(network_override), + ); + } + } + } else { + println!("Invalid phrase/URI given"); + } +} + +/// Try to parse given `public` as hex encoded public key and print relevant information. +pub fn print_from_public( + public_str: &str, + network_override: Option, + output: OutputType, +) -> Result<(), Error> +where + Pair: sp_core::Pair, + Pair::Public: Into, +{ + let public = array_bytes::hex2bytes(public_str)?; + + let public_key = Pair::Public::try_from(&public) + .map_err(|_| "Failed to construct public key from given hex")?; + + let network_override = unwrap_or_default_ss58_version(network_override); + + match output { + OutputType::Json => { + let json = json!({ + "networkId": String::from(network_override), + "publicKey": format_public_key::(public_key.clone()), + "accountId": format_account_id::(public_key.clone()), + "ss58PublicKey": public_key.to_ss58check_with_version(network_override), + "ss58Address": public_key.to_ss58check_with_version(network_override), + }); + + println!( + "{}", + serde_json::to_string_pretty(&json).expect("Json pretty print failed") + ); + } + OutputType::Text => { + println!( + "Network ID/Version: {}\n \ + Public key (hex): {}\n \ + Account ID: {}\n \ + Public key (SS58): {}\n \ + SS58 Address: {}", + String::from(network_override), + format_public_key::(public_key.clone()), + format_account_id::(public_key.clone()), + public_key.to_ss58check_with_version(network_override), + public_key.to_ss58check_with_version(network_override), + ); + } + } + + Ok(()) +} + +/// generate a pair from suri +pub fn pair_from_suri(suri: &str, password: Option) -> Result { + let result = if let Some(pass) = password { + let mut pass_str = pass.expose_secret().clone(); + let pair = P::from_string(suri, Some(&pass_str)); + pass_str.zeroize(); + pair + } else { + P::from_string(suri, None) + }; + + Ok(result.map_err(|err| format!("Invalid phrase {:?}", err))?) +} + +/// formats seed as hex +pub fn format_seed(seed: SeedFor

) -> String { + format!("0x{}", HexDisplay::from(&seed.as_ref())) +} + +/// formats public key as hex +fn format_public_key(public_key: PublicFor

) -> String { + format!("0x{}", HexDisplay::from(&public_key.as_ref())) +} + +/// formats public key as accountId as hex +fn format_account_id(public_key: PublicFor

) -> String +where + PublicFor

: Into, +{ + format!( + "0x{}", + HexDisplay::from(&public_key.into().into_account().as_ref()) + ) +} + +/// Allows for calling $method with appropriate crypto impl. +#[macro_export] +macro_rules! with_crypto_scheme { + ( + $scheme:expr, + $method:ident ( $($params:expr),* $(,)?) $(,)? + ) => { + $crate::with_crypto_scheme!($scheme, $method<>($($params),*)) + }; + ( + $scheme:expr, + $method:ident<$($generics:ty),*>( $( $params:expr ),* $(,)?) $(,)? + ) => { + match $scheme { + $crate::CryptoScheme::Ecdsa => { + $method::($($params),*) + } + $crate::CryptoScheme::Sr25519 => { + $method::($($params),*) + } + $crate::CryptoScheme::Ed25519 => { + $method::($($params),*) + } + } + }; +} diff --git a/substrate/cli/src/commands/vanity.rs b/substrate/cli/src/commands/vanity.rs new file mode 100644 index 00000000000..fbd4d3db091 --- /dev/null +++ b/substrate/cli/src/commands/vanity.rs @@ -0,0 +1,244 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! implementation of the `vanity` subcommand + +use crate::{ + error, utils, with_crypto_scheme, CryptoSchemeFlag, NetworkSchemeFlag, OutputTypeFlag, +}; +use clap::Parser; +use rand::{rngs::OsRng, RngCore}; +use sp_core::crypto::{unwrap_or_default_ss58_version, Ss58AddressFormat, Ss58Codec}; +use sp_runtime::traits::IdentifyAccount; +use utils::print_from_uri; + +/// The `vanity` command +#[derive(Debug, Clone, Parser)] +#[command( + name = "vanity", + about = "Generate a seed that provides a vanity address" +)] +pub struct VanityCmd { + /// Desired pattern + #[arg(long, value_parser = assert_non_empty_string)] + pattern: String, + + #[allow(missing_docs)] + #[clap(flatten)] + network_scheme: NetworkSchemeFlag, + + #[allow(missing_docs)] + #[clap(flatten)] + output_scheme: OutputTypeFlag, + + #[allow(missing_docs)] + #[clap(flatten)] + crypto_scheme: CryptoSchemeFlag, +} + +impl VanityCmd { + /// Run the command + pub fn run(&self) -> error::Result<()> { + let formatted_seed = with_crypto_scheme!( + self.crypto_scheme.scheme, + generate_key( + &self.pattern, + unwrap_or_default_ss58_version(self.network_scheme.network) + ), + )?; + + with_crypto_scheme!( + self.crypto_scheme.scheme, + print_from_uri( + &formatted_seed, + None, + self.network_scheme.network, + self.output_scheme.output_type, + ), + ); + Ok(()) + } +} + +/// genertae a key based on given pattern +fn generate_key( + desired: &str, + network_override: Ss58AddressFormat, +) -> Result +where + Pair: sp_core::Pair, + Pair::Public: IdentifyAccount, + ::AccountId: Ss58Codec, +{ + println!("Generating key containing pattern '{}'", desired); + + let top = 45 + (desired.len() * 48); + let mut best = 0; + let mut seed = Pair::Seed::default(); + let mut done = 0; + + loop { + if done % 100000 == 0 { + OsRng.fill_bytes(seed.as_mut()); + } else { + next_seed(seed.as_mut()); + } + + let p = Pair::from_seed(&seed); + let ss58 = p + .public() + .into_account() + .to_ss58check_with_version(network_override); + let score = calculate_score(desired, &ss58); + if score > best || desired.len() < 2 { + best = score; + if best >= top { + println!("best: {} == top: {}", best, top); + return Ok(utils::format_seed::(seed.clone())); + } + } + done += 1; + + if done % good_waypoint(done) == 0 { + println!("{} keys searched; best is {}/{} complete", done, best, top); + } + } +} + +fn good_waypoint(done: u64) -> u64 { + match done { + 0..=1_000_000 => 100_000, + 1_000_001..=10_000_000 => 1_000_000, + 10_000_001..=100_000_000 => 10_000_000, + 100_000_001.. => 100_000_000, + } +} + +fn next_seed(seed: &mut [u8]) { + for s in seed { + match s { + 255 => { + *s = 0; + } + _ => { + *s += 1; + break; + } + } + } +} + +/// Calculate the score of a key based on the desired +/// input. +fn calculate_score(_desired: &str, key: &str) -> usize { + for truncate in 0.._desired.len() { + let snip_size = _desired.len() - truncate; + let truncated = &_desired[0..snip_size]; + if let Some(pos) = key.find(truncated) { + return (47 - pos) + (snip_size * 48); + } + } + 0 +} + +/// checks that `pattern` is non-empty +fn assert_non_empty_string(pattern: &str) -> Result { + if pattern.is_empty() { + Err("Pattern must not be empty") + } else { + Ok(pattern.to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use sp_core::{ + crypto::{default_ss58_version, Ss58AddressFormatRegistry, Ss58Codec}, + sr25519, Pair, + }; + #[cfg(feature = "bench")] + use test::Bencher; + + #[test] + fn vanity() { + let vanity = VanityCmd::parse_from(["vanity", "--pattern", "j"]); + assert!(vanity.run().is_ok()); + } + + #[test] + fn test_generation_with_single_char() { + let seed = generate_key::("ab", default_ss58_version()).unwrap(); + assert!( + sr25519::Pair::from_seed_slice(&array_bytes::hex2bytes_unchecked(&seed)) + .unwrap() + .public() + .to_ss58check() + .contains("ab") + ); + } + + #[test] + fn generate_key_respects_network_override() { + let seed = + generate_key::("ab", Ss58AddressFormatRegistry::PolkadotAccount.into()) + .unwrap(); + assert!( + sr25519::Pair::from_seed_slice(&array_bytes::hex2bytes_unchecked(&seed)) + .unwrap() + .public() + .to_ss58check_with_version(Ss58AddressFormatRegistry::PolkadotAccount.into()) + .contains("ab") + ); + } + + #[test] + fn test_score_1_char_100() { + let score = calculate_score("j", "5jolkadotwHY5k9GpdTgpqs9xjuNvtv8EcwCFpEeyEf3KHim"); + assert_eq!(score, 94); + } + + #[test] + fn test_score_100() { + let score = calculate_score( + "Polkadot", + "5PolkadotwHY5k9GpdTgpqs9xjuNvtv8EcwCFpEeyEf3KHim", + ); + assert_eq!(score, 430); + } + + #[test] + fn test_score_50_2() { + // 50% for the position + 50% for the size + assert_eq!( + calculate_score( + "Polkadot", + "5PolkXXXXwHY5k9GpdTgpqs9xjuNvtv8EcwCFpEeyEf3KHim" + ), + 238 + ); + } + + #[test] + fn test_score_0() { + assert_eq!( + calculate_score( + "Polkadot", + "5GUWv4bLCchGUHJrzULXnh4JgXsMpTKRnjuXTY7Qo1Kh9uYK" + ), + 0 + ); + } + + #[cfg(feature = "bench")] + #[bench] + fn bench_paranoiac(b: &mut Bencher) { + b.iter(|| generate_key("polk")); + } + + #[cfg(feature = "bench")] + #[bench] + fn bench_not_paranoiac(b: &mut Bencher) { + b.iter(|| generate_key("polk")); + } +} diff --git a/substrate/cli/src/commands/verify.rs b/substrate/cli/src/commands/verify.rs new file mode 100644 index 00000000000..e82ebae92ea --- /dev/null +++ b/substrate/cli/src/commands/verify.rs @@ -0,0 +1,129 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! implementation of the `verify` subcommand + +use crate::{error, params::MessageParams, utils, with_crypto_scheme, CryptoSchemeFlag}; +use clap::Parser; +use sp_core::crypto::{ByteArray, Ss58Codec}; +use std::io::BufRead; + +/// The `verify` command +#[derive(Debug, Clone, Parser)] +#[command( + name = "verify", + about = "Verify a signature for a message, provided on STDIN, with a given (public or secret) key" +)] +pub struct VerifyCmd { + /// Signature, hex-encoded. + sig: String, + + /// The public or secret key URI. + /// If the value is a file, the file content is used as URI. + /// If not given, you will be prompted for the URI. + uri: Option, + + #[allow(missing_docs)] + #[clap(flatten)] + pub message_params: MessageParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub crypto_scheme: CryptoSchemeFlag, +} + +impl VerifyCmd { + /// Run the command + pub fn run(&self) -> error::Result<()> { + self.verify(|| std::io::stdin().lock()) + } + + /// Verify a signature for a message. + /// + /// The message can either be provided as immediate argument via CLI or otherwise read from the + /// reader created by `create_reader`. The reader will only be created in case that the message + /// is not passed as immediate. + pub(crate) fn verify(&self, create_reader: F) -> error::Result<()> + where + R: BufRead, + F: FnOnce() -> R, + { + let message = self.message_params.message_from(create_reader)?; + let sig_data = array_bytes::hex2bytes(&self.sig)?; + let uri = utils::read_uri(self.uri.as_ref())?; + let uri = if let Some(uri) = uri.strip_prefix("0x") { + uri + } else { + &uri + }; + + with_crypto_scheme!(self.crypto_scheme.scheme, verify(sig_data, message, uri)) + } +} + +fn verify(sig_data: Vec, message: Vec, uri: &str) -> error::Result<()> +where + Pair: sp_core::Pair, + Pair::Signature: for<'a> TryFrom<&'a [u8]>, +{ + let signature = + Pair::Signature::try_from(&sig_data).map_err(|_| error::Error::SignatureFormatInvalid)?; + + let pubkey = if let Ok(pubkey_vec) = array_bytes::hex2bytes(uri) { + Pair::Public::from_slice(pubkey_vec.as_slice()) + .map_err(|_| error::Error::KeyFormatInvalid)? + } else { + Pair::Public::from_string(uri)? + }; + + if Pair::verify(&signature, &message, &pubkey) { + println!("Signature verifies correctly."); + } else { + return Err(error::Error::SignatureInvalid); + } + + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + + const ALICE: &str = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"; + const SIG1: &str = "0x4eb25a2285a82374888880af0024eb30c3a21ce086eae3862888d345af607f0ad6fb081312f11730932564f24a9f8ebcee2d46861413ae61307eca58db2c3e81"; + const SIG2: &str = "0x026342225155056ea797118c1c8c8b3cc002aa2020c36f4217fa3c302783a572ad3dcd38c231cbaf86cadb93984d329c963ceac0685cc1ee4c1ed50fa443a68f"; + + // Verify work with `--message` argument. + #[test] + fn verify_immediate() { + let cmd = VerifyCmd::parse_from(["verify", SIG1, ALICE, "--message", "test message"]); + assert!(cmd.run().is_ok(), "Alice' signature should verify"); + } + + // Verify work without `--message` argument. + #[test] + fn verify_stdin() { + let cmd = VerifyCmd::parse_from(["verify", SIG1, ALICE]); + let message = "test message"; + assert!( + cmd.verify(|| message.as_bytes()).is_ok(), + "Alice' signature should verify" + ); + } + + // Verify work with `--message` argument for hex message. + #[test] + fn verify_immediate_hex() { + let cmd = VerifyCmd::parse_from(["verify", SIG2, ALICE, "--message", "0xaabbcc", "--hex"]); + assert!(cmd.run().is_ok(), "Alice' signature should verify"); + } + + // Verify work without `--message` argument for hex message. + #[test] + fn verify_stdin_hex() { + let cmd = VerifyCmd::parse_from(["verify", SIG2, ALICE, "--hex"]); + assert!(cmd.verify(|| "0xaabbcc".as_bytes()).is_ok()); + assert!(cmd.verify(|| "aabbcc".as_bytes()).is_ok()); + assert!(cmd.verify(|| "0xaABBcC".as_bytes()).is_ok()); + } +} diff --git a/substrate/cli/src/config.rs b/substrate/cli/src/config.rs new file mode 100644 index 00000000000..ae1d7070f3f --- /dev/null +++ b/substrate/cli/src/config.rs @@ -0,0 +1,736 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Configuration trait for a CLI based on substrate + +use crate::{ + arg_enums::Database, error::Result, DatabaseParams, ImportParams, KeystoreParams, + NetworkParams, NodeKeyParams, OffchainWorkerParams, PruningParams, RpcEndpoint, SharedParams, + SubstrateCli, +}; +use log::warn; +use names::{Generator, Name}; +use sc_service::{ + config::{ + BasePath, Configuration, DatabaseSource, ExecutorConfiguration, IpNetwork, KeystoreConfig, + NetworkConfiguration, NodeKeyConfig, OffchainWorkerConfig, PrometheusConfig, PruningMode, + Role, RpcBatchRequestConfig, RpcConfiguration, RpcMethodLimit, RpcMethods, + TelemetryEndpoints, TransactionPoolOptions, WasmExecutionMethod, + }, + BlocksPruning, ChainSpec, TracingReceiver, +}; +use sc_tracing::logging::LoggerBuilder; +use std::{num::NonZeroU32, path::PathBuf}; + +/// The maximum number of characters for a node name. +pub(crate) const NODE_NAME_MAX_LENGTH: usize = 64; + +/// Default sub directory to store network config. +pub(crate) const DEFAULT_NETWORK_CONFIG_PATH: &str = "network"; + +/// The recommended open file descriptor limit to be configured for the process. +const RECOMMENDED_OPEN_FILE_DESCRIPTOR_LIMIT: u64 = 10_000; + +/// The default port. +pub const RPC_DEFAULT_PORT: u16 = 9944; +/// The default max number of subscriptions per connection. +pub const RPC_DEFAULT_MAX_SUBS_PER_CONN: u32 = 1024; +/// The default max request size in MB. +pub const RPC_DEFAULT_MAX_REQUEST_SIZE_MB: u32 = 15; +/// The default max response size in MB. +pub const RPC_DEFAULT_MAX_RESPONSE_SIZE_MB: u32 = 15; +/// The default concurrent connection limit. +pub const RPC_DEFAULT_MAX_CONNECTIONS: u32 = 100; +/// The default number of messages the RPC server +/// is allowed to keep in memory per connection. +pub const RPC_DEFAULT_MESSAGE_CAPACITY_PER_CONN: u32 = 64; + +/// Default configuration values used by Substrate +/// +/// These values will be used by [`CliConfiguration`] to set +/// default values for e.g. the listen port or the RPC port. +pub trait DefaultConfigurationValues { + /// The port Substrate should listen on for p2p connections. + /// + /// By default this is `30333`. + fn p2p_listen_port() -> u16 { + 30333 + } + + /// The port Substrate should listen on for JSON-RPC connections. + /// + /// By default this is `9944`. + fn rpc_listen_port() -> u16 { + RPC_DEFAULT_PORT + } + + /// The port Substrate should listen on for prometheus connections. + /// + /// By default this is `9615`. + fn prometheus_listen_port() -> u16 { + 9615 + } +} + +impl DefaultConfigurationValues for () {} + +/// A trait that allows converting an object to a Configuration +pub trait CliConfiguration: Sized { + /// Get the SharedParams for this object + fn shared_params(&self) -> &SharedParams; + + /// Get the ImportParams for this object + fn import_params(&self) -> Option<&ImportParams> { + None + } + + /// Get the PruningParams for this object + fn pruning_params(&self) -> Option<&PruningParams> { + self.import_params().map(|x| &x.pruning_params) + } + + /// Get the KeystoreParams for this object + fn keystore_params(&self) -> Option<&KeystoreParams> { + None + } + + /// Get the NetworkParams for this object + fn network_params(&self) -> Option<&NetworkParams> { + None + } + + /// Get a reference to `OffchainWorkerParams` for this object. + fn offchain_worker_params(&self) -> Option<&OffchainWorkerParams> { + None + } + + /// Get the NodeKeyParams for this object + fn node_key_params(&self) -> Option<&NodeKeyParams> { + self.network_params().map(|x| &x.node_key_params) + } + + /// Get the DatabaseParams for this object + fn database_params(&self) -> Option<&DatabaseParams> { + self.import_params().map(|x| &x.database_params) + } + + /// Get the base path of the configuration (if any) + /// + /// By default this is retrieved from `SharedParams`. + fn base_path(&self) -> Result> { + self.shared_params().base_path() + } + + /// Returns `true` if the node is for development or not + /// + /// By default this is retrieved from `SharedParams`. + fn is_dev(&self) -> Result { + Ok(self.shared_params().is_dev()) + } + + /// Gets the role + /// + /// By default this is `Role::Full`. + fn role(&self, _is_dev: bool) -> Result { + Ok(Role::Full) + } + + /// Get the transaction pool options + /// + /// By default this is `TransactionPoolOptions::default()`. + fn transaction_pool(&self, _is_dev: bool) -> Result { + Ok(Default::default()) + } + + /// Get the network configuration + /// + /// By default this is retrieved from `NetworkParams` if it is available otherwise it creates + /// a default `NetworkConfiguration` based on `node_name`, `client_id`, `node_key` and + /// `net_config_dir`. + fn network_config( + &self, + chain_spec: &Box, + is_dev: bool, + is_validator: bool, + net_config_dir: PathBuf, + client_id: &str, + node_name: &str, + node_key: NodeKeyConfig, + default_listen_port: u16, + ) -> Result { + let network_config = if let Some(network_params) = self.network_params() { + network_params.network_config( + chain_spec, + is_dev, + is_validator, + Some(net_config_dir), + client_id, + node_name, + node_key, + default_listen_port, + ) + } else { + NetworkConfiguration::new(node_name, client_id, node_key, Some(net_config_dir)) + }; + + // TODO: Return error here in the next release: + // https://github.com/paritytech/polkadot-sdk/issues/5266 + // if is_validator && network_config.public_addresses.is_empty() {} + + Ok(network_config) + } + + /// Get the keystore configuration. + /// + /// By default this is retrieved from `KeystoreParams` if it is available. Otherwise it uses + /// `KeystoreConfig::InMemory`. + fn keystore_config(&self, config_dir: &PathBuf) -> Result { + self.keystore_params() + .map(|x| x.keystore_config(config_dir)) + .unwrap_or_else(|| Ok(KeystoreConfig::InMemory)) + } + + /// Get the database cache size. + /// + /// By default this is retrieved from `DatabaseParams` if it is available. Otherwise its `None`. + fn database_cache_size(&self) -> Result> { + Ok(self + .database_params() + .map(|x| x.database_cache_size()) + .unwrap_or_default()) + } + + /// Get the database backend variant. + /// + /// By default this is retrieved from `DatabaseParams` if it is available. Otherwise its `None`. + fn database(&self) -> Result> { + Ok(self.database_params().and_then(|x| x.database())) + } + + /// Get the database configuration object for the parameters provided + fn database_config( + &self, + base_path: &PathBuf, + cache_size: usize, + database: Database, + ) -> Result { + let role_dir = "full"; + let rocksdb_path = base_path.join("db").join(role_dir); + let paritydb_path = base_path.join("paritydb").join(role_dir); + Ok(match database { + #[cfg(feature = "rocksdb")] + Database::RocksDb => DatabaseSource::RocksDb { + path: rocksdb_path, + cache_size, + }, + Database::ParityDb => DatabaseSource::ParityDb { + path: paritydb_path, + }, + Database::ParityDbDeprecated => { + eprintln!( + "WARNING: \"paritydb-experimental\" database setting is deprecated and will be removed in future releases. \ + Please update your setup to use the new value: \"paritydb\"." + ); + DatabaseSource::ParityDb { + path: paritydb_path, + } + } + Database::Auto => DatabaseSource::Auto { + paritydb_path, + rocksdb_path, + cache_size, + }, + }) + } + + /// Get the trie cache maximum size. + /// + /// By default this is retrieved from `ImportParams` if it is available. Otherwise its `0`. + /// If `None` is returned the trie cache is disabled. + fn trie_cache_maximum_size(&self) -> Result> { + Ok(self + .import_params() + .map(|x| x.trie_cache_maximum_size()) + .unwrap_or_default()) + } + + /// Get the state pruning mode. + /// + /// By default this is retrieved from `PruningMode` if it is available. Otherwise its + /// `PruningMode::default()`. + fn state_pruning(&self) -> Result> { + self.pruning_params() + .map(|x| x.state_pruning()) + .unwrap_or_else(|| Ok(Default::default())) + } + + /// Get the block pruning mode. + /// + /// By default this is retrieved from `block_pruning` if it is available. Otherwise its + /// `BlocksPruning::KeepFinalized`. + fn blocks_pruning(&self) -> Result { + self.pruning_params() + .map(|x| x.blocks_pruning()) + .unwrap_or_else(|| Ok(BlocksPruning::KeepFinalized)) + } + + /// Get the chain ID (string). + /// + /// By default this is retrieved from `SharedParams`. + fn chain_id(&self, is_dev: bool) -> Result { + Ok(self.shared_params().chain_id(is_dev)) + } + + /// Get the name of the node. + /// + /// By default a random name is generated. + fn node_name(&self) -> Result { + Ok(generate_node_name()) + } + + /// Get the WASM execution method. + /// + /// By default this is retrieved from `ImportParams` if it is available. Otherwise its + /// `WasmExecutionMethod::default()`. + fn wasm_method(&self) -> Result { + Ok(self + .import_params() + .map(|x| x.wasm_method()) + .unwrap_or_default()) + } + + /// Get the path where WASM overrides live. + /// + /// By default this is `None`. + fn wasm_runtime_overrides(&self) -> Option { + self.import_params() + .map(|x| x.wasm_runtime_overrides()) + .unwrap_or_default() + } + + /// Get the RPC address. + fn rpc_addr(&self, _default_listen_port: u16) -> Result>> { + Ok(None) + } + + /// Returns the RPC method set to expose. + /// + /// By default this is `RpcMethods::Auto` (unsafe RPCs are denied iff + /// `rpc_external` returns true, respectively). + fn rpc_methods(&self) -> Result { + Ok(Default::default()) + } + + /// Get the maximum number of RPC server connections. + fn rpc_max_connections(&self) -> Result { + Ok(RPC_DEFAULT_MAX_CONNECTIONS) + } + + /// Get the RPC cors (`None` if disabled) + /// + /// By default this is `Some(Vec::new())`. + fn rpc_cors(&self, _is_dev: bool) -> Result>> { + Ok(Some(Vec::new())) + } + + /// Get maximum RPC request payload size. + fn rpc_max_request_size(&self) -> Result { + Ok(RPC_DEFAULT_MAX_REQUEST_SIZE_MB) + } + + /// Get maximum RPC response payload size. + fn rpc_max_response_size(&self) -> Result { + Ok(RPC_DEFAULT_MAX_RESPONSE_SIZE_MB) + } + + /// Get maximum number of subscriptions per connection. + fn rpc_max_subscriptions_per_connection(&self) -> Result { + Ok(RPC_DEFAULT_MAX_SUBS_PER_CONN) + } + + /// The number of messages the RPC server is allowed to keep in memory per connection. + fn rpc_buffer_capacity_per_connection(&self) -> Result { + Ok(RPC_DEFAULT_MESSAGE_CAPACITY_PER_CONN) + } + + /// RPC server batch request configuration. + fn rpc_batch_config(&self) -> Result { + Ok(RpcBatchRequestConfig::Unlimited) + } + + /// RPC rate limit configuration. + fn rpc_rate_limit(&self) -> Result> { + Ok(None) + } + + /// RPC rate limit whitelisted ip addresses. + fn rpc_rate_limit_whitelisted_ips(&self) -> Result> { + Ok(vec![]) + } + + /// RPC rate limit trust proxy headers. + fn rpc_rate_limit_trust_proxy_headers(&self) -> Result { + Ok(false) + } + + /// Node-wide per-method RPC rate and concurrency budgets. + /// + /// Aliases share one budget across all HTTP and WebSocket connections. An empty vector leaves + /// all methods unlimited. + fn rpc_method_limits(&self) -> Result> { + Ok(Vec::new()) + } + + /// Get the prometheus configuration (`None` if disabled) + /// + /// By default this is `None`. + fn prometheus_config( + &self, + _default_listen_port: u16, + _chain_spec: &Box, + ) -> Result> { + Ok(None) + } + + /// Get the telemetry endpoints (if any) + /// + /// By default this is retrieved from the chain spec loaded by `load_spec`. + fn telemetry_endpoints( + &self, + chain_spec: &Box, + ) -> Result> { + Ok(chain_spec.telemetry_endpoints().clone()) + } + + /// Get the default value for heap pages + /// + /// By default this is `None`. + fn default_heap_pages(&self) -> Result> { + Ok(None) + } + + /// Returns an offchain worker config wrapped in `Ok(_)` + /// + /// By default offchain workers are disabled. + fn offchain_worker(&self, role: &Role) -> Result { + self.offchain_worker_params() + .map(|x| x.offchain_worker(role)) + .unwrap_or_else(|| Ok(OffchainWorkerConfig::default())) + } + + /// Returns `Ok(true)` if authoring should be forced + /// + /// By default this is `false`. + fn force_authoring(&self) -> Result { + Ok(Default::default()) + } + + /// Returns `Ok(true)` if grandpa should be disabled + /// + /// By default this is `false`. + fn disable_grandpa(&self) -> Result { + Ok(Default::default()) + } + + /// Get the development key seed from the current object + /// + /// By default this is `None`. + fn dev_key_seed(&self, _is_dev: bool) -> Result> { + Ok(Default::default()) + } + + /// Get the tracing targets from the current object (if any) + /// + /// By default this is retrieved from [`SharedParams`] if it is available. Otherwise its + /// `None`. + fn tracing_targets(&self) -> Result> { + Ok(self.shared_params().tracing_targets()) + } + + /// Get the TracingReceiver value from the current object + /// + /// By default this is retrieved from [`SharedParams`] if it is available. Otherwise its + /// `TracingReceiver::default()`. + fn tracing_receiver(&self) -> Result { + Ok(self.shared_params().tracing_receiver()) + } + + /// Get the node key from the current object + /// + /// By default this is retrieved from `NodeKeyParams` if it is available. Otherwise its + /// `NodeKeyConfig::default()`. + fn node_key(&self, net_config_dir: &PathBuf) -> Result { + let is_dev = self.is_dev()?; + let role = self.role(is_dev)?; + self.node_key_params() + .map(|x| x.node_key(net_config_dir, role, is_dev)) + .unwrap_or_else(|| Ok(Default::default())) + } + + /// Get maximum runtime instances + /// + /// By default this is `None`. + fn max_runtime_instances(&self) -> Result> { + Ok(Default::default()) + } + + /// Get maximum different runtimes in cache + /// + /// By default this is `2`. + fn runtime_cache_size(&self) -> Result { + Ok(2) + } + + /// Activate or not the automatic announcing of blocks after import + /// + /// By default this is `false`. + fn announce_block(&self) -> Result { + Ok(true) + } + + /// Create a Configuration object from the current object + fn create_configuration( + &self, + cli: &C, + tokio_handle: tokio::runtime::Handle, + ) -> Result { + let is_dev = self.is_dev()?; + let chain_id = self.chain_id(is_dev)?; + let chain_spec = cli.load_spec(&chain_id)?; + let base_path = base_path_or_default(self.base_path()?, &C::executable_name()); + let config_dir = build_config_dir(&base_path, chain_spec.id()); + let net_config_dir = build_net_config_dir(&config_dir); + let client_id = C::client_id(); + let database_cache_size = self.database_cache_size()?.unwrap_or(1024); + let database = self.database()?.unwrap_or( + #[cfg(feature = "rocksdb")] + { + Database::RocksDb + }, + #[cfg(not(feature = "rocksdb"))] + { + Database::ParityDb + }, + ); + let node_key = self.node_key(&net_config_dir)?; + let role = self.role(is_dev)?; + let max_runtime_instances = self.max_runtime_instances()?.unwrap_or(8); + let is_validator = role.is_authority(); + let keystore = self.keystore_config(&config_dir)?; + let telemetry_endpoints = self.telemetry_endpoints(&chain_spec)?; + let runtime_cache_size = self.runtime_cache_size()?; + + let rpc_addrs: Option> = self + .rpc_addr(DCV::rpc_listen_port())? + .map(|addrs| addrs.into_iter().map(Into::into).collect()); + + Ok(Configuration { + impl_name: C::impl_name(), + impl_version: C::impl_version(), + tokio_handle, + transaction_pool: self.transaction_pool(is_dev)?, + network: self.network_config( + &chain_spec, + is_dev, + is_validator, + net_config_dir, + client_id.as_str(), + self.node_name()?.as_str(), + node_key, + DCV::p2p_listen_port(), + )?, + keystore, + database: self.database_config(&config_dir, database_cache_size, database)?, + data_path: config_dir, + trie_cache_maximum_size: self.trie_cache_maximum_size()?, + state_pruning: self.state_pruning()?, + blocks_pruning: self.blocks_pruning()?, + executor: ExecutorConfiguration { + wasm_method: self.wasm_method()?, + default_heap_pages: self.default_heap_pages()?, + max_runtime_instances, + runtime_cache_size, + }, + wasm_runtime_overrides: self.wasm_runtime_overrides(), + rpc: RpcConfiguration { + addr: rpc_addrs, + methods: self.rpc_methods()?, + max_connections: self.rpc_max_connections()?, + cors: self.rpc_cors(is_dev)?, + max_request_size: self.rpc_max_request_size()?, + max_response_size: self.rpc_max_response_size()?, + id_provider: None, + max_subs_per_conn: self.rpc_max_subscriptions_per_connection()?, + port: DCV::rpc_listen_port(), + message_buffer_capacity: self.rpc_buffer_capacity_per_connection()?, + batch_config: self.rpc_batch_config()?, + method_limits: self.rpc_method_limits()?, + rate_limit: self.rpc_rate_limit()?, + rate_limit_whitelisted_ips: self.rpc_rate_limit_whitelisted_ips()?, + rate_limit_trust_proxy_headers: self.rpc_rate_limit_trust_proxy_headers()?, + }, + prometheus_config: self + .prometheus_config(DCV::prometheus_listen_port(), &chain_spec)?, + telemetry_endpoints, + offchain_worker: self.offchain_worker(&role)?, + force_authoring: self.force_authoring()?, + disable_grandpa: self.disable_grandpa()?, + dev_key_seed: self.dev_key_seed(is_dev)?, + tracing_targets: self.tracing_targets()?, + tracing_receiver: self.tracing_receiver()?, + chain_spec, + announce_block: self.announce_block()?, + role, + base_path, + }) + } + + /// Get the filters for the logging. + /// + /// This should be a list of comma-separated values. + /// Example: `foo=trace,bar=debug,baz=info` + /// + /// By default this is retrieved from `SharedParams`. + fn log_filters(&self) -> Result { + Ok(self.shared_params().log_filters().join(",")) + } + + /// Should the detailed log output be enabled. + fn detailed_log_output(&self) -> Result { + Ok(self.shared_params().detailed_log_output()) + } + + /// Is log reloading enabled? + fn enable_log_reloading(&self) -> Result { + Ok(self.shared_params().enable_log_reloading()) + } + + /// Should the log color output be disabled? + fn disable_log_color(&self) -> Result { + Ok(self.shared_params().disable_log_color()) + } + + /// Initialize substrate. This must be done only once per process. + /// + /// This method: + /// + /// 1. Sets the panic handler + /// 2. Optionally customize logger/profiling + /// 2. Initializes the logger + /// 3. Raises the FD limit + /// + /// The `logger_hook` closure is executed before the logger is constructed + /// and initialized. It is useful for setting up a custom profiler. + /// + /// Example: + /// ``` + /// use sc_tracing::{SpanDatum, TraceEvent}; + /// struct TestProfiler; + /// + /// impl sc_tracing::TraceHandler for TestProfiler { + /// fn handle_span(&self, sd: &SpanDatum) {} + /// fn handle_event(&self, _event: &TraceEvent) {} + /// }; + /// + /// fn logger_hook() -> impl FnOnce(&mut sc_cli::LoggerBuilder, &sc_service::Configuration) -> () { + /// |logger_builder, config| { + /// logger_builder.with_custom_profiling(Box::new(TestProfiler {})); + /// } + /// } + /// ``` + fn init(&self, support_url: &String, impl_version: &String, logger_hook: F) -> Result<()> + where + F: FnOnce(&mut LoggerBuilder), + { + sp_panic_handler::set(support_url, impl_version); + + let mut logger = LoggerBuilder::new(self.log_filters()?); + logger + .with_log_reloading(self.enable_log_reloading()?) + .with_detailed_output(self.detailed_log_output()?); + + if let Some(tracing_targets) = self.tracing_targets()? { + let tracing_receiver = self.tracing_receiver()?; + logger.with_profiling(tracing_receiver, tracing_targets); + } + + if self.disable_log_color()? { + logger.with_colors(false); + } + + // Call hook for custom profiling setup. + logger_hook(&mut logger); + + logger.init()?; + + match fdlimit::raise_fd_limit() { + Ok(fdlimit::Outcome::LimitRaised { to, .. }) => { + if to < RECOMMENDED_OPEN_FILE_DESCRIPTOR_LIMIT { + warn!( + "Low open file descriptor limit configured for the process. \ + Current value: {:?}, recommended value: {:?}.", + to, RECOMMENDED_OPEN_FILE_DESCRIPTOR_LIMIT, + ); + } + } + Ok(fdlimit::Outcome::Unsupported) => { + // Unsupported platform (non-Linux) + } + Err(error) => { + warn!( + "Failed to configure file descriptor limit for the process: \ + {}, recommended value: {:?}.", + error, RECOMMENDED_OPEN_FILE_DESCRIPTOR_LIMIT, + ); + } + } + + Ok(()) + } +} + +/// Generate a valid random name for the node +pub fn generate_node_name() -> String { + loop { + let node_name = Generator::with_naming(Name::Numbered) + .next() + .expect("RNG is available on all supported platforms; qed"); + let count = node_name.chars().count(); + + if count < NODE_NAME_MAX_LENGTH { + return node_name; + } + } +} + +/// Returns the value of `base_path` or the default_path if it is None +pub(crate) fn base_path_or_default( + base_path: Option, + executable_name: &String, +) -> BasePath { + base_path.unwrap_or_else(|| BasePath::from_project("", "", executable_name)) +} + +/// Returns the default path for configuration directory based on the chain_spec +pub(crate) fn build_config_dir(base_path: &BasePath, chain_spec_id: &str) -> PathBuf { + base_path.config_dir(chain_spec_id) +} + +/// Returns the default path for the network configuration inside the configuration dir +pub(crate) fn build_net_config_dir(config_dir: &PathBuf) -> PathBuf { + config_dir.join(DEFAULT_NETWORK_CONFIG_PATH) +} + +/// Returns the default path for the network directory starting from the provided base_path +/// or from the default base_path. +pub(crate) fn build_network_key_dir_or_default( + base_path: Option, + chain_spec_id: &str, + executable_name: &String, +) -> PathBuf { + let config_dir = build_config_dir( + &base_path_or_default(base_path, executable_name), + chain_spec_id, + ); + build_net_config_dir(&config_dir) +} diff --git a/substrate/cli/src/error.rs b/substrate/cli/src/error.rs new file mode 100644 index 00000000000..5d307006f64 --- /dev/null +++ b/substrate/cli/src/error.rs @@ -0,0 +1,106 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Initialization errors. + +use std::path::PathBuf; + +use sp_core::crypto; + +/// Result type alias for the CLI. +pub type Result = std::result::Result; + +/// Error type for the CLI. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + + #[error(transparent)] + Cli(#[from] clap::Error), + + #[error(transparent)] + Service(#[from] sc_service::Error), + + #[error(transparent)] + Client(#[from] sp_blockchain::Error), + + #[error(transparent)] + Codec(#[from] codec::Error), + + #[error("Invalid input: {0}")] + Input(String), + + #[error("Invalid listen multiaddress")] + InvalidListenMultiaddress, + + #[error("Invalid URI; expecting either a secret URI or a public URI.")] + InvalidUri(crypto::PublicError), + + #[error("Signature is an invalid format.")] + SignatureFormatInvalid, + + #[error("Key is an invalid format.")] + KeyFormatInvalid, + + #[error("Unknown key type, must be a known 4-character sequence")] + KeyTypeInvalid, + + #[error("Signature verification failed")] + SignatureInvalid, + + #[error("Key store operation failed")] + KeystoreOperation, + + #[error("Key storage issue encountered")] + KeyStorage(#[from] sc_keystore::Error), + + #[error("Invalid hexadecimal string data, {0:?}")] + HexDataConversion(array_bytes::Error), + + /// Application specific error chain sequence forwarder. + #[error(transparent)] + Application(#[from] Box), + + #[error(transparent)] + GlobalLoggerError(#[from] sc_tracing::logging::Error), + + #[error( + "Starting an authority without network key in {0}. + \n This is not a safe operation because other authorities in the network may depend on your node having a stable identity. + \n Otherwise these other authorities may not being able to reach you. + \n If it is the first time running your node you could use one of the following methods: + \n 1. [Preferred] Separately generate the key with: key generate-node-key --base-path + \n 2. [Preferred] Separately generate the key with: key generate-node-key --file + \n 3. [Preferred] Separately generate the key with: key generate-node-key --default-base-path + \n 4. [Unsafe] Pass --unsafe-force-node-key-generation and make sure you remove it for subsequent node restarts" + )] + NetworkKeyNotFound(PathBuf), + #[error("A network key already exists in path {0}")] + KeyAlreadyExistsInPath(PathBuf), +} + +impl From<&str> for Error { + fn from(s: &str) -> Error { + Error::Input(s.to_string()) + } +} + +impl From for Error { + fn from(s: String) -> Error { + Error::Input(s) + } +} + +impl From for Error { + fn from(e: crypto::PublicError) -> Error { + Error::InvalidUri(e) + } +} + +impl From for Error { + fn from(e: array_bytes::Error) -> Error { + Error::HexDataConversion(e) + } +} diff --git a/substrate/cli/src/lib.rs b/substrate/cli/src/lib.rs new file mode 100644 index 00000000000..aba7bdd8cfa --- /dev/null +++ b/substrate/cli/src/lib.rs @@ -0,0 +1,251 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Substrate CLI library. +//! +//! To see a full list of commands available, see [`commands`]. + +#![warn(missing_docs)] +#![warn(unused_extern_crates)] +#![warn(unused_imports)] +#![allow( + unexpected_cfgs, + clippy::borrowed_box, + clippy::ptr_arg, + clippy::result_large_err, + clippy::too_many_arguments, + reason = "Preserve the pinned Polkadot SDK CLI API while vendoring the crate" +)] + +use clap::{CommandFactory, FromArgMatches, Parser}; +use log::warn; +use sc_service::Configuration; + +pub mod arg_enums; +pub mod commands; +mod config; +mod error; +mod params; +mod runner; +mod signals; + +pub use arg_enums::*; +pub use clap; +pub use commands::*; +pub use config::*; +pub use error::*; +pub use params::*; +pub use runner::*; +pub use sc_service::{ChainSpec, Role}; +pub use sc_tracing::logging::LoggerBuilder; +pub use signals::Signals; +pub use sp_version::RuntimeVersion; + +/// Substrate client CLI +/// +/// This trait needs to be implemented on the root CLI struct of the application. It will provide +/// the implementation `name`, `version`, `executable name`, `description`, `author`, `support_url`, +/// `copyright start year` and most importantly: how to load the chain spec. +pub trait SubstrateCli: Sized { + /// Implementation name. + fn impl_name() -> String; + + /// Implementation version. + /// + /// By default, it will look like this: + /// + /// `2.0.0-b950f731c` + /// + /// Where the hash is the short hash of the commit in the Git repository. + fn impl_version() -> String; + + /// Executable file name. + /// + /// Extracts the file name from `std::env::current_exe()`. + /// Resorts to the env var `CARGO_PKG_NAME` in case of Error. + fn executable_name() -> String { + std::env::current_exe() + .ok() + .and_then(|e| e.file_name().map(|s| s.to_os_string())) + .and_then(|w| w.into_string().ok()) + .unwrap_or_else(|| env!("CARGO_PKG_NAME").into()) + } + + /// Executable file description. + fn description() -> String; + + /// Executable file author. + fn author() -> String; + + /// Support URL. + fn support_url() -> String; + + /// Copyright starting year (x-current year) + fn copyright_start_year() -> i32; + + /// Chain spec factory + fn load_spec(&self, id: &str) -> std::result::Result, String>; + + /// Helper function used to parse the command line arguments. This is the equivalent of + /// [`clap::Parser::parse()`]. + /// + /// To allow running the node without subcommand, it also sets a few more settings: + /// [`clap::Command::propagate_version`], [`clap::Command::args_conflicts_with_subcommands`], + /// [`clap::Command::subcommand_negates_reqs`]. + /// + /// Creates `Self` from the command line arguments. Print the + /// error message and quit the program in case of failure. + fn from_args() -> Self + where + Self: Parser + Sized, + { + ::from_iter(std::env::args_os()) + } + + /// Helper function used to parse the command line arguments. This is the equivalent of + /// [`clap::Parser::parse_from`]. + /// + /// To allow running the node without subcommand, it also sets a few more settings: + /// [`clap::Command::propagate_version`], [`clap::Command::args_conflicts_with_subcommands`], + /// [`clap::Command::subcommand_negates_reqs`]. + /// + /// Creates `Self` from any iterator over arguments. + /// Print the error message and quit the program in case of failure. + fn from_iter(iter: I) -> Self + where + Self: Parser + Sized, + I: IntoIterator, + I::Item: Into + Clone, + { + let app = ::command(); + + let mut full_version = Self::impl_version(); + full_version.push('\n'); + + let name = Self::executable_name(); + let author = Self::author(); + let about = Self::description(); + let app = app + .name(name) + .author(author) + .about(about) + .version(full_version) + .propagate_version(true) + .args_conflicts_with_subcommands(true) + .subcommand_negates_reqs(true); + + let matches = app.try_get_matches_from(iter).unwrap_or_else(|e| e.exit()); + + ::from_arg_matches(&matches).unwrap_or_else(|e| e.exit()) + } + + /// Helper function used to parse the command line arguments. This is the equivalent of + /// [`clap::Parser::try_parse_from`] + /// + /// To allow running the node without subcommand, it also sets a few more settings: + /// [`clap::Command::propagate_version`], [`clap::Command::args_conflicts_with_subcommands`], + /// [`clap::Command::subcommand_negates_reqs`]. + /// + /// Creates `Self` from any iterator over arguments. + /// Print the error message and quit the program in case of failure. + /// + /// **NOTE:** This method WILL NOT exit when `--help` or `--version` (or short versions) are + /// used. It will return a [`clap::Error`], where the [`clap::Error::kind`] is a + /// [`clap::error::ErrorKind::DisplayHelp`] or [`clap::error::ErrorKind::DisplayVersion`] + /// respectively. You must call [`clap::Error::exit`] or perform a [`std::process::exit`]. + fn try_from_iter(iter: I) -> clap::error::Result + where + Self: Parser + Sized, + I: IntoIterator, + I::Item: Into + Clone, + { + let app = ::command(); + + let mut full_version = Self::impl_version(); + full_version.push('\n'); + + let name = Self::executable_name(); + let author = Self::author(); + let about = Self::description(); + let app = app + .name(name) + .author(author) + .about(about) + .version(full_version); + + let matches = app.try_get_matches_from(iter)?; + + ::from_arg_matches(&matches) + } + + /// Returns the client ID: `{impl_name}/v{impl_version}` + fn client_id() -> String { + format!("{}/v{}", Self::impl_name(), Self::impl_version()) + } + + /// Only create a Configuration for the command provided in argument + fn create_configuration, DVC: DefaultConfigurationValues>( + &self, + command: &T, + tokio_handle: tokio::runtime::Handle, + ) -> error::Result { + command.create_configuration(self, tokio_handle) + } + + /// Create a runner for the command provided in argument. This will create a Configuration and + /// a tokio runtime + fn create_runner, DVC: DefaultConfigurationValues>( + &self, + command: &T, + ) -> Result> { + self.create_runner_with_logger_hook(command, |_, _| {}) + } + + /// Create a runner for the command provided in argument. The `logger_hook` can be used to setup + /// a custom profiler or update the logger configuration before it is initialized. + /// + /// Example: + /// ``` + /// use sc_tracing::{SpanDatum, TraceEvent}; + /// struct TestProfiler; + /// + /// impl sc_tracing::TraceHandler for TestProfiler { + /// fn handle_span(&self, sd: &SpanDatum) {} + /// fn handle_event(&self, _event: &TraceEvent) {} + /// }; + /// + /// fn logger_hook() -> impl FnOnce(&mut sc_cli::LoggerBuilder, &sc_service::Configuration) -> () { + /// |logger_builder, config| { + /// logger_builder.with_custom_profiling(Box::new(TestProfiler {})); + /// } + /// } + /// ``` + fn create_runner_with_logger_hook< + T: CliConfiguration, + DVC: DefaultConfigurationValues, + F, + >( + &self, + command: &T, + logger_hook: F, + ) -> Result> + where + F: FnOnce(&mut LoggerBuilder, &Configuration), + { + let tokio_runtime = build_runtime()?; + + // `capture` needs to be called in a tokio context. + // Also capture them as early as possible. + let signals = tokio_runtime.block_on(async { Signals::capture() })?; + + let config = command.create_configuration(self, tokio_runtime.handle().clone())?; + + command.init( + &Self::support_url(), + &Self::impl_version(), + |logger_builder| logger_hook(logger_builder, &config), + )?; + + Runner::new(config, tokio_runtime, signals) + } +} diff --git a/substrate/cli/src/params/database_params.rs b/substrate/cli/src/params/database_params.rs new file mode 100644 index 00000000000..64fb72a2d89 --- /dev/null +++ b/substrate/cli/src/params/database_params.rs @@ -0,0 +1,29 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::arg_enums::Database; +use clap::Args; + +/// Parameters for database +#[derive(Debug, Clone, PartialEq, Args)] +pub struct DatabaseParams { + /// Select database backend to use. + #[arg(long, alias = "db", value_name = "DB", ignore_case = true, value_enum)] + pub database: Option, + + /// Limit the memory the database cache can use. + #[arg(long = "db-cache", value_name = "MiB")] + pub database_cache_size: Option, +} + +impl DatabaseParams { + /// Database backend + pub fn database(&self) -> Option { + self.database + } + + /// Limit the memory the database cache can use. + pub fn database_cache_size(&self) -> Option { + self.database_cache_size + } +} diff --git a/substrate/cli/src/params/import_params.rs b/substrate/cli/src/params/import_params.rs new file mode 100644 index 00000000000..5df314c7a95 --- /dev/null +++ b/substrate/cli/src/params/import_params.rs @@ -0,0 +1,164 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::{ + arg_enums::{ + ExecutionStrategy, WasmExecutionMethod, WasmtimeInstantiationStrategy, + DEFAULT_WASMTIME_INSTANTIATION_STRATEGY, DEFAULT_WASM_EXECUTION_METHOD, + }, + params::{DatabaseParams, PruningParams}, +}; +use clap::Args; +use std::path::PathBuf; + +/// Parameters for block import. +#[derive(Debug, Clone, Args)] +pub struct ImportParams { + #[allow(missing_docs)] + #[clap(flatten)] + pub pruning_params: PruningParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub database_params: DatabaseParams, + + /// Method for executing Wasm runtime code. + #[arg( + long = "wasm-execution", + value_name = "METHOD", + value_enum, + ignore_case = true, + default_value_t = DEFAULT_WASM_EXECUTION_METHOD, + )] + pub wasm_method: WasmExecutionMethod, + + /// The WASM instantiation method to use. + /// + /// Only has an effect when `wasm-execution` is set to `compiled`. + /// The copy-on-write strategies are only supported on Linux. + /// If the copy-on-write variant of a strategy is unsupported + /// the executor will fall back to the non-CoW equivalent. + /// The fastest (and the default) strategy available is `pooling-copy-on-write`. + /// The `legacy-instance-reuse` strategy is deprecated and will + /// be removed in the future. It should only be used in case of + /// issues with the default instantiation strategy. + #[arg( + long, + value_name = "STRATEGY", + default_value_t = DEFAULT_WASMTIME_INSTANTIATION_STRATEGY, + value_enum, + )] + pub wasmtime_instantiation_strategy: WasmtimeInstantiationStrategy, + + /// Specify the path where local WASM runtimes are stored. + /// + /// These runtimes will override on-chain runtimes when the version matches. + #[arg(long, value_name = "PATH")] + pub wasm_runtime_overrides: Option, + + #[allow(missing_docs)] + #[clap(flatten)] + pub execution_strategies: ExecutionStrategiesParams, + + /// Specify the state cache size. + /// + /// Providing `0` will disable the cache. + #[arg(long, value_name = "Bytes", default_value_t = 67108864)] + pub trie_cache_size: usize, + + /// DEPRECATED: switch to `--trie-cache-size`. + #[arg(long)] + state_cache_size: Option, +} + +impl ImportParams { + /// Specify the trie cache maximum size. + pub fn trie_cache_maximum_size(&self) -> Option { + if self.state_cache_size.is_some() { + eprintln!("`--state-cache-size` was deprecated. Please switch to `--trie-cache-size`."); + } + + if self.trie_cache_size == 0 { + None + } else { + Some(self.trie_cache_size) + } + } + + /// Get the WASM execution method from the parameters + pub fn wasm_method(&self) -> sc_service::config::WasmExecutionMethod { + self.execution_strategies + .check_usage_and_print_deprecation_warning(); + + crate::execution_method_from_cli(self.wasm_method, self.wasmtime_instantiation_strategy) + } + + /// Enable overriding on-chain WASM with locally-stored WASM + /// by specifying the path where local WASM is stored. + pub fn wasm_runtime_overrides(&self) -> Option { + self.wasm_runtime_overrides.clone() + } +} + +/// Execution strategies parameters. +#[derive(Debug, Clone, Args)] +pub struct ExecutionStrategiesParams { + /// Runtime execution strategy for importing blocks during initial sync. + #[arg(long, value_name = "STRATEGY", value_enum, ignore_case = true)] + pub execution_syncing: Option, + + /// Runtime execution strategy for general block import (including locally authored blocks). + #[arg(long, value_name = "STRATEGY", value_enum, ignore_case = true)] + pub execution_import_block: Option, + + /// Runtime execution strategy for constructing blocks. + #[arg(long, value_name = "STRATEGY", value_enum, ignore_case = true)] + pub execution_block_construction: Option, + + /// Runtime execution strategy for offchain workers. + #[arg(long, value_name = "STRATEGY", value_enum, ignore_case = true)] + pub execution_offchain_worker: Option, + + /// Runtime execution strategy when not syncing, importing or constructing blocks. + #[arg(long, value_name = "STRATEGY", value_enum, ignore_case = true)] + pub execution_other: Option, + + /// The execution strategy that should be used by all execution contexts. + #[arg( + long, + value_name = "STRATEGY", + value_enum, + ignore_case = true, + conflicts_with_all = &[ + "execution_other", + "execution_offchain_worker", + "execution_block_construction", + "execution_import_block", + "execution_syncing", + ] + )] + pub execution: Option, +} + +impl ExecutionStrategiesParams { + /// Check if one of the parameters is still passed and print a warning if so. + fn check_usage_and_print_deprecation_warning(&self) { + for (param, name) in [ + (&self.execution_syncing, "execution-syncing"), + (&self.execution_import_block, "execution-import-block"), + ( + &self.execution_block_construction, + "execution-block-construction", + ), + (&self.execution_offchain_worker, "execution-offchain-worker"), + (&self.execution_other, "execution-other"), + (&self.execution, "execution"), + ] { + if param.is_some() { + eprintln!( + "CLI parameter `--{name}` has no effect anymore and will be removed in the future!" + ); + } + } + } +} diff --git a/substrate/cli/src/params/keystore_params.rs b/substrate/cli/src/params/keystore_params.rs new file mode 100644 index 00000000000..d2f45309dd1 --- /dev/null +++ b/substrate/cli/src/params/keystore_params.rs @@ -0,0 +1,88 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::{error, error::Result}; +use clap::Args; +use sc_service::config::KeystoreConfig; +use sp_core::crypto::SecretString; +use std::{ + fs, + path::{Path, PathBuf}, +}; + +/// default sub directory for the key store +const DEFAULT_KEYSTORE_CONFIG_PATH: &str = "keystore"; + +/// Parameters of the keystore +#[derive(Debug, Clone, Args)] +pub struct KeystoreParams { + /// Specify custom keystore path. + #[arg(long, value_name = "PATH")] + pub keystore_path: Option, + + /// Use interactive shell for entering the password used by the keystore. + #[arg(long, conflicts_with_all = &["password", "password_filename"])] + pub password_interactive: bool, + + /// Password used by the keystore. + /// + /// This allows appending an extra user-defined secret to the seed. + #[arg( + long, + value_parser = secret_string_from_str, + conflicts_with_all = &["password_interactive", "password_filename"] + )] + pub password: Option, + + /// File that contains the password used by the keystore. + #[arg( + long, + value_name = "PATH", + conflicts_with_all = &["password_interactive", "password"] + )] + pub password_filename: Option, +} + +/// Parse a secret string, returning a displayable error. +pub fn secret_string_from_str(s: &str) -> std::result::Result { + std::str::FromStr::from_str(s).map_err(|_| "Could not get SecretString".to_string()) +} + +impl KeystoreParams { + /// Get the keystore configuration for the parameters + pub fn keystore_config(&self, config_dir: &Path) -> Result { + let password = if self.password_interactive { + Some(SecretString::new(input_keystore_password()?)) + } else if let Some(ref file) = self.password_filename { + let password = fs::read_to_string(file).map_err(|e| format!("{}", e))?; + Some(SecretString::new(password)) + } else { + self.password.clone() + }; + + let path = self + .keystore_path + .clone() + .unwrap_or_else(|| config_dir.join(DEFAULT_KEYSTORE_CONFIG_PATH)); + + Ok(KeystoreConfig::Path { path, password }) + } + + /// helper method to fetch password from `KeyParams` or read from stdin + pub fn read_password(&self) -> error::Result> { + let (password_interactive, password) = (self.password_interactive, self.password.clone()); + + let pass = if password_interactive { + let password = rpassword::prompt_password("Key password: ")?; + Some(SecretString::new(password)) + } else { + password + }; + + Ok(pass) + } +} + +fn input_keystore_password() -> Result { + rpassword::prompt_password("Keystore password: ").map_err(|e| format!("{:?}", e).into()) +} diff --git a/substrate/cli/src/params/message_params.rs b/substrate/cli/src/params/message_params.rs new file mode 100644 index 00000000000..82ff5553cd8 --- /dev/null +++ b/substrate/cli/src/params/message_params.rs @@ -0,0 +1,139 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Params to configure how a message should be passed into a command. + +use crate::error::Error; +use array_bytes::{hex2bytes, hex_bytes2hex_str}; +use clap::Args; +use std::io::BufRead; + +/// Params to configure how a message should be passed into a command. +#[derive(Debug, Clone, Args)] +pub struct MessageParams { + /// Message to process. Will be read from STDIN otherwise. + /// The message is assumed to be raw bytes per default. Use `--hex` for hex input. Can + /// optionally be prefixed with `0x` in the hex case. + #[arg(long)] + message: Option, + + /// The message is hex-encoded data. + #[arg(long)] + hex: bool, +} + +impl MessageParams { + /// Produces the message by either using its immediate value or reading from stdin. + /// + /// This function should only be called once and the result cached. + pub(crate) fn message_from(&self, create_reader: F) -> Result, Error> + where + R: BufRead, + F: FnOnce() -> R, + { + let raw = match &self.message { + Some(raw) => raw.as_bytes().to_vec(), + None => { + let mut raw = vec![]; + create_reader().read_to_end(&mut raw)?; + raw + } + }; + if self.hex { + hex2bytes(hex_bytes2hex_str(&raw)?).map_err(Into::into) + } else { + Ok(raw) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Test that decoding an immediate message works. + #[test] + fn message_decode_immediate() { + for (name, input, hex, output) in test_closures() { + println!("Testing: immediate_{}", name); + let params = MessageParams { + message: Some(input.into()), + hex, + }; + let message = params.message_from(|| std::io::stdin().lock()); + + match output { + Some(output) => { + let message = + message.unwrap_or_else(|_| panic!("{}: should decode but did not", name)); + assert_eq!(message, output, "{}: decoded a wrong message", name); + } + None => { + message + .err() + .unwrap_or_else(|| panic!("{}: should not decode but did", name)); + } + } + } + } + + /// Test that decoding a message from a stream works. + #[test] + fn message_decode_stream() { + for (name, input, hex, output) in test_closures() { + println!("Testing: stream_{}", name); + let params = MessageParams { message: None, hex }; + let message = params.message_from(|| input.as_bytes()); + + match output { + Some(output) => { + let message = + message.unwrap_or_else(|_| panic!("{}: should decode but did not", name)); + assert_eq!(message, output, "{}: decoded a wrong message", name); + } + None => { + message + .err() + .unwrap_or_else(|| panic!("{}: should not decode but did", name)); + } + } + } + } + + /// Returns (test_name, input, hex, output). + fn test_closures() -> Vec<(&'static str, &'static str, bool, Option<&'static [u8]>)> { + vec![ + ( + "decode_no_hex_works", + "Hello this is not hex", + false, + Some(b"Hello this is not hex"), + ), + ( + "decode_no_hex_with_hex_string_works", + "0xffffffff", + false, + Some(b"0xffffffff"), + ), + ( + "decode_hex_works", + "0x00112233", + true, + Some(&[0, 17, 34, 51]), + ), + ( + "decode_hex_without_prefix_works", + "00112233", + true, + Some(&[0, 17, 34, 51]), + ), + ( + "decode_hex_uppercase_works", + "0xaAbbCCDd", + true, + Some(&[170, 187, 204, 221]), + ), + ("decode_hex_wrong_len_errors", "0x0011223", true, None), + ] + } +} diff --git a/substrate/cli/src/params/mixnet_params.rs b/substrate/cli/src/params/mixnet_params.rs new file mode 100644 index 00000000000..f5dd7ff637b --- /dev/null +++ b/substrate/cli/src/params/mixnet_params.rs @@ -0,0 +1,54 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use clap::Args; +use sp_core::H256; +use std::str::FromStr; + +fn parse_kx_secret(s: &str) -> Result { + H256::from_str(s) + .map(H256::to_fixed_bytes) + .map_err(|err| err.to_string()) +} + +/// Parameters used to create the mixnet configuration. +#[derive(Debug, Clone, Args)] +pub struct MixnetParams { + /// Enable the mixnet service. + /// + /// This will make the mixnet RPC methods available. If the node is running as a validator, it + /// will also attempt to register and operate as a mixnode. + #[arg(long)] + pub mixnet: bool, + + /// The mixnet key-exchange secret to use in session 0. + /// + /// Should be 64 hex characters, giving a 32-byte secret. + /// + /// WARNING: Secrets provided as command-line arguments are easily exposed. Use of this option + /// should be limited to development and testing. + #[arg(long, value_name = "SECRET", value_parser = parse_kx_secret)] + pub mixnet_session_0_kx_secret: Option, +} + +impl MixnetParams { + /// Returns the mixnet configuration, or `None` if the mixnet is disabled. + pub fn config(&self, is_authority: bool) -> Option { + self.mixnet.then(|| { + let mut config = sc_mixnet::Config { + core: sc_mixnet::CoreConfig { + session_0_kx_secret: self.mixnet_session_0_kx_secret, + ..Default::default() + }, + ..Default::default() + }; + if !is_authority { + // Only authorities can be mixnodes; don't attempt to register + config.substrate.register = false; + // Only mixnodes need to allow connections from non-mixnodes + config.substrate.num_gateway_slots = 0; + } + config + }) + } +} diff --git a/substrate/cli/src/params/mod.rs b/substrate/cli/src/params/mod.rs new file mode 100644 index 00000000000..abce8ec0552 --- /dev/null +++ b/substrate/cli/src/params/mod.rs @@ -0,0 +1,191 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +mod database_params; +mod import_params; +mod keystore_params; +mod message_params; +mod mixnet_params; +mod network_params; +mod node_key_params; +mod offchain_worker_params; +mod prometheus_params; +mod pruning_params; +mod rpc_params; +mod runtime_params; +mod shared_params; +mod telemetry_params; +mod transaction_pool_params; + +use crate::arg_enums::{CryptoScheme, OutputType}; +use clap::Args; +use sc_service::config::{IpNetwork, RpcBatchRequestConfig}; +use sp_core::crypto::{Ss58AddressFormat, Ss58AddressFormatRegistry}; +use sp_runtime::{ + generic::BlockId, + traits::{Block as BlockT, NumberFor}, +}; +use std::{fmt::Debug, str::FromStr}; + +pub use crate::params::{ + database_params::*, import_params::*, keystore_params::*, message_params::*, mixnet_params::*, + network_params::*, node_key_params::*, offchain_worker_params::*, prometheus_params::*, + pruning_params::*, rpc_params::*, runtime_params::*, shared_params::*, telemetry_params::*, + transaction_pool_params::*, +}; + +/// Parse Ss58AddressFormat +pub fn parse_ss58_address_format(x: &str) -> Result { + match Ss58AddressFormatRegistry::try_from(x) { + Ok(format_registry) => Ok(format_registry.into()), + Err(_) => Err(format!( + "Unable to parse variant. Known variants: {:?}", + Ss58AddressFormat::all_names() + )), + } +} + +/// Wrapper type of `String` that holds an unsigned integer of arbitrary size, formatted as a +/// decimal. +#[derive(Debug, Clone)] +pub struct GenericNumber(String); + +impl FromStr for GenericNumber { + type Err = String; + + fn from_str(block_number: &str) -> Result { + if let Some(pos) = block_number.chars().position(|d| !d.is_ascii_digit()) { + Err(format!( + "Expected block number, found illegal digit at position: {}", + pos + )) + } else { + Ok(Self(block_number.to_owned())) + } + } +} + +impl GenericNumber { + /// Wrapper on top of `std::str::parse` but with `Error` as a `String` + /// + /// See `https://doc.rust-lang.org/std/primitive.str.html#method.parse` for more elaborate + /// documentation. + pub fn parse(&self) -> Result + where + N: FromStr, + N::Err: std::fmt::Debug, + { + FromStr::from_str(&self.0).map_err(|e| format!("Failed to parse block number: {:?}", e)) + } +} + +/// Wrapper type that is either a `Hash` or the number of a `Block`. +#[derive(Debug, Clone)] +pub struct BlockNumberOrHash(String); + +impl FromStr for BlockNumberOrHash { + type Err = String; + + fn from_str(block_number: &str) -> Result { + if let Some(rest) = block_number.strip_prefix("0x") { + if let Some(pos) = rest.chars().position(|c| !c.is_ascii_hexdigit()) { + Err(format!( + "Expected block hash, found illegal hex character at position: {}", + 2 + pos, + )) + } else { + Ok(Self(block_number.into())) + } + } else { + GenericNumber::from_str(block_number).map(|v| Self(v.0)) + } + } +} + +impl BlockNumberOrHash { + /// Parse the inner value as `BlockId`. + pub fn parse(&self) -> Result, String> + where + ::Err: std::fmt::Debug, + NumberFor: FromStr, + as FromStr>::Err: std::fmt::Debug, + { + if self.0.starts_with("0x") { + Ok(BlockId::Hash(FromStr::from_str(&self.0[2..]).map_err( + |e| format!("Failed to parse block hash: {:?}", e), + )?)) + } else { + GenericNumber(self.0.clone()).parse().map(BlockId::Number) + } + } +} + +/// Optional flag for specifying crypto algorithm +#[derive(Debug, Clone, Args)] +pub struct CryptoSchemeFlag { + /// cryptography scheme + #[arg(long, value_name = "SCHEME", value_enum, ignore_case = true, default_value_t = CryptoScheme::Sr25519)] + pub scheme: CryptoScheme, +} + +/// Optional flag for specifying output type +#[derive(Debug, Clone, Args)] +pub struct OutputTypeFlag { + /// output format + #[arg(long, value_name = "FORMAT", value_enum, ignore_case = true, default_value_t = OutputType::Text)] + pub output_type: OutputType, +} + +/// Optional flag for specifying network scheme +#[derive(Debug, Clone, Args)] +pub struct NetworkSchemeFlag { + /// network address format + #[arg( + short = 'n', + long, + value_name = "NETWORK", + ignore_case = true, + value_parser = parse_ss58_address_format, + )] + pub network: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + type Header = sp_runtime::generic::Header; + type Block = sp_runtime::generic::Block; + + #[test] + fn parse_block_number() { + let block_number_or_hash = BlockNumberOrHash::from_str("1234").unwrap(); + let parsed = block_number_or_hash.parse::().unwrap(); + assert_eq!(BlockId::Number(1234), parsed); + } + + #[test] + fn parse_block_hash() { + let hash = sp_core::H256::default(); + let hash_str = format!("{:?}", hash); + let block_number_or_hash = BlockNumberOrHash::from_str(&hash_str).unwrap(); + let parsed = block_number_or_hash.parse::().unwrap(); + assert_eq!(BlockId::Hash(hash), parsed); + } + + #[test] + fn parse_block_hash_fails() { + assert_eq!( + "Expected block hash, found illegal hex character at position: 2", + BlockNumberOrHash::from_str("0xHello").unwrap_err(), + ); + } + + #[test] + fn parse_block_number_fails() { + assert_eq!( + "Expected block number, found illegal digit at position: 3", + BlockNumberOrHash::from_str("345Hello").unwrap_err(), + ); + } +} diff --git a/substrate/cli/src/params/network_params.rs b/substrate/cli/src/params/network_params.rs new file mode 100644 index 00000000000..087e70c1a09 --- /dev/null +++ b/substrate/cli/src/params/network_params.rs @@ -0,0 +1,322 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::{ + arg_enums::{NetworkBackendType, SyncMode}, + params::node_key_params::NodeKeyParams, +}; +use clap::Args; +use sc_network::{ + config::{ + NetworkConfiguration, NodeKeyConfig, NonReservedPeerMode, SetConfig, TransportConfig, + }, + multiaddr::Protocol, +}; +use sc_service::{ + config::{Multiaddr, MultiaddrWithPeerId}, + ChainSpec, ChainType, +}; +use std::{borrow::Cow, num::NonZeroUsize, path::PathBuf}; + +/// Parameters used to create the network configuration. +#[derive(Debug, Clone, Args)] +pub struct NetworkParams { + /// Specify a list of bootnodes. + #[arg(long, value_name = "ADDR", num_args = 1..)] + pub bootnodes: Vec, + + /// Specify a list of reserved node addresses. + #[arg(long, value_name = "ADDR", num_args = 1..)] + pub reserved_nodes: Vec, + + /// Whether to only synchronize the chain with reserved nodes. + /// + /// Also disables automatic peer discovery. + /// TCP connections might still be established with non-reserved nodes. + /// In particular, if you are a validator your node might still connect to other + /// validator nodes and collator nodes regardless of whether they are defined as + /// reserved nodes. + #[arg(long)] + pub reserved_only: bool, + + /// Public address that other nodes will use to connect to this node. + /// + /// This can be used if there's a proxy in front of this node. + #[arg(long, value_name = "PUBLIC_ADDR", num_args = 1..)] + pub public_addr: Vec, + + /// Listen on this multiaddress. + /// + /// By default: + /// If `--validator` is passed: `/ip4/0.0.0.0/tcp/` and `/ip6/[::]/tcp/`. + /// Otherwise: `/ip4/0.0.0.0/tcp//ws` and `/ip6/[::]/tcp//ws`. + #[arg(long, value_name = "LISTEN_ADDR", num_args = 1..)] + pub listen_addr: Vec, + + /// Specify p2p protocol TCP port. + #[arg(long, value_name = "PORT", conflicts_with_all = &[ "listen_addr" ])] + pub port: Option, + + /// Always forbid connecting to private IPv4/IPv6 addresses. + /// + /// The option doesn't apply to addresses passed with `--reserved-nodes` or + /// `--bootnodes`. Enabled by default for chains marked as "live" in their chain + /// specifications. + /// + /// Address allocation for private networks is specified by + /// [RFC1918](https://tools.ietf.org/html/rfc1918)). + #[arg(long, alias = "no-private-ipv4", conflicts_with_all = &["allow_private_ip"])] + pub no_private_ip: bool, + + /// Always accept connecting to private IPv4/IPv6 addresses. + /// + /// Enabled by default for chains marked as "local" in their chain specifications, + /// or when `--dev` is passed. + /// + /// Address allocation for private networks is specified by + /// [RFC1918](https://tools.ietf.org/html/rfc1918)). + #[arg(long, alias = "allow-private-ipv4", conflicts_with_all = &["no_private_ip"])] + pub allow_private_ip: bool, + + /// Number of outgoing connections we're trying to maintain. + #[arg(long, value_name = "COUNT", default_value_t = 8)] + pub out_peers: u32, + + /// Maximum number of inbound full nodes peers. + #[arg(long, value_name = "COUNT", default_value_t = 32)] + pub in_peers: u32, + + /// Maximum number of inbound light nodes peers. + #[arg(long, value_name = "COUNT", default_value_t = 100)] + pub in_peers_light: u32, + + /// Disable mDNS discovery (default: true). + /// + /// By default, the network will use mDNS to discover other nodes on the + /// local network. This disables it. Automatically implied when using --dev. + #[arg(long)] + pub no_mdns: bool, + + /// Maximum number of peers from which to ask for the same blocks in parallel. + /// + /// This allows downloading announced blocks from multiple peers. + /// Decrease to save traffic and risk increased latency. + #[arg(long, value_name = "COUNT", default_value_t = 5)] + pub max_parallel_downloads: u32, + + #[allow(missing_docs)] + #[clap(flatten)] + pub node_key_params: NodeKeyParams, + + /// Enable peer discovery on local networks. + /// + /// By default this option is `true` for `--dev` or when the chain type is + /// `Local`/`Development` and false otherwise. + #[arg(long)] + pub discover_local: bool, + + /// Require iterative Kademlia DHT queries to use disjoint paths. + /// + /// Disjoint paths increase resiliency in the presence of potentially adversarial nodes. + /// + /// See the S/Kademlia paper for more information on the high level design as well as its + /// security improvements. + #[arg(long)] + pub kademlia_disjoint_query_paths: bool, + + /// Kademlia replication factor. + /// + /// Determines to how many closest peers a record is replicated to. + /// + /// Discovery mechanism requires successful replication to all + /// `kademlia_replication_factor` peers to consider record successfully put. + #[arg(long, default_value = "20")] + pub kademlia_replication_factor: NonZeroUsize, + + /// Join the IPFS network and serve transactions over bitswap protocol. + #[arg(long)] + pub ipfs_server: bool, + + /// Blockchain syncing mode. + #[arg( + long, + value_enum, + value_name = "SYNC_MODE", + default_value_t = SyncMode::Full, + ignore_case = true, + verbatim_doc_comment + )] + pub sync: SyncMode, + + /// Maximum number of blocks per request. + /// + /// Try reducing this number from the default value if you have a slow network connection + /// and observe block requests timing out. + #[arg(long, value_name = "COUNT", default_value_t = 64)] + pub max_blocks_per_request: u32, + + /// Network backend used for P2P networking. + /// + /// litep2p network backend is considered experimental and isn't as stable as the libp2p + /// network backend. + #[arg( + long, + value_enum, + value_name = "NETWORK_BACKEND", + default_value_t = NetworkBackendType::Libp2p, + ignore_case = true, + verbatim_doc_comment + )] + pub network_backend: NetworkBackendType, +} + +impl NetworkParams { + /// Fill the given `NetworkConfiguration` by looking at the cli parameters. + pub fn network_config( + &self, + chain_spec: &Box, + is_dev: bool, + is_validator: bool, + net_config_path: Option, + client_id: &str, + node_name: &str, + node_key: NodeKeyConfig, + default_listen_port: u16, + ) -> NetworkConfiguration { + let port = self.port.unwrap_or(default_listen_port); + + let listen_addresses = if self.listen_addr.is_empty() { + if is_validator || is_dev { + vec![ + Multiaddr::empty() + .with(Protocol::Ip6([0, 0, 0, 0, 0, 0, 0, 0].into())) + .with(Protocol::Tcp(port)), + Multiaddr::empty() + .with(Protocol::Ip4([0, 0, 0, 0].into())) + .with(Protocol::Tcp(port)), + ] + } else { + vec![ + Multiaddr::empty() + .with(Protocol::Ip6([0, 0, 0, 0, 0, 0, 0, 0].into())) + .with(Protocol::Tcp(port)) + .with(Protocol::Ws(Cow::Borrowed("/"))), + Multiaddr::empty() + .with(Protocol::Ip4([0, 0, 0, 0].into())) + .with(Protocol::Tcp(port)) + .with(Protocol::Ws(Cow::Borrowed("/"))), + ] + } + } else { + self.listen_addr.clone() + }; + + let public_addresses = self.public_addr.clone(); + + let mut boot_nodes = chain_spec.boot_nodes().to_vec(); + boot_nodes.extend(self.bootnodes.clone()); + + let chain_type = chain_spec.chain_type(); + // Activate if the user explicitly requested local discovery, `--dev` is given or the + // chain type is `Local`/`Development` + let allow_non_globals_in_dht = self.discover_local + || is_dev + || matches!(chain_type, ChainType::Local | ChainType::Development); + + let allow_private_ip = match (self.allow_private_ip, self.no_private_ip) { + (true, true) => unreachable!("`*_private_ip` flags are mutually exclusive; qed"), + (true, false) => true, + (false, true) => false, + (false, false) => { + is_dev || matches!(chain_type, ChainType::Local | ChainType::Development) + } + }; + + NetworkConfiguration { + boot_nodes, + net_config_path, + default_peers_set: SetConfig { + in_peers: self.in_peers + self.in_peers_light, + out_peers: self.out_peers, + reserved_nodes: self.reserved_nodes.clone(), + non_reserved_mode: if self.reserved_only { + NonReservedPeerMode::Deny + } else { + NonReservedPeerMode::Accept + }, + }, + default_peers_set_num_full: self.in_peers + self.out_peers, + listen_addresses, + public_addresses, + node_key, + node_name: node_name.to_string(), + client_version: client_id.to_string(), + transport: TransportConfig::Normal { + enable_mdns: !is_dev && !self.no_mdns, + allow_private_ip, + }, + max_parallel_downloads: self.max_parallel_downloads, + max_blocks_per_request: self.max_blocks_per_request, + enable_dht_random_walk: !self.reserved_only, + allow_non_globals_in_dht, + kademlia_disjoint_query_paths: self.kademlia_disjoint_query_paths, + kademlia_replication_factor: self.kademlia_replication_factor, + yamux_window_size: None, + ipfs_server: self.ipfs_server, + sync_mode: self.sync.into(), + network_backend: self.network_backend.into(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::Parser; + + #[derive(Parser)] + struct Cli { + #[clap(flatten)] + network_params: NetworkParams, + } + + #[test] + fn reserved_nodes_multiple_values_and_occurrences() { + let params = Cli::try_parse_from([ + "", + "--reserved-nodes", + "/ip4/0.0.0.0/tcp/501/p2p/12D3KooWEBo1HUPQJwiBmM5kSeg4XgiVxEArArQdDarYEsGxMfbS", + "/ip4/0.0.0.0/tcp/502/p2p/12D3KooWEBo1HUPQJwiBmM5kSeg4XgiVxEArArQdDarYEsGxMfbS", + "--reserved-nodes", + "/ip4/0.0.0.0/tcp/503/p2p/12D3KooWEBo1HUPQJwiBmM5kSeg4XgiVxEArArQdDarYEsGxMfbS", + ]) + .expect("Parses network params"); + + let expected = vec![ + MultiaddrWithPeerId::try_from( + "/ip4/0.0.0.0/tcp/501/p2p/12D3KooWEBo1HUPQJwiBmM5kSeg4XgiVxEArArQdDarYEsGxMfbS" + .to_string(), + ) + .unwrap(), + MultiaddrWithPeerId::try_from( + "/ip4/0.0.0.0/tcp/502/p2p/12D3KooWEBo1HUPQJwiBmM5kSeg4XgiVxEArArQdDarYEsGxMfbS" + .to_string(), + ) + .unwrap(), + MultiaddrWithPeerId::try_from( + "/ip4/0.0.0.0/tcp/503/p2p/12D3KooWEBo1HUPQJwiBmM5kSeg4XgiVxEArArQdDarYEsGxMfbS" + .to_string(), + ) + .unwrap(), + ]; + + assert_eq!(expected, params.network_params.reserved_nodes); + } + + #[test] + fn sync_ignores_case() { + let params = Cli::try_parse_from(["", "--sync", "wArP"]).expect("Parses network params"); + + assert_eq!(SyncMode::Warp, params.network_params.sync); + } +} diff --git a/substrate/cli/src/params/node_key_params.rs b/substrate/cli/src/params/node_key_params.rs new file mode 100644 index 00000000000..b95f8106063 --- /dev/null +++ b/substrate/cli/src/params/node_key_params.rs @@ -0,0 +1,282 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use clap::Args; +use sc_network::config::{ed25519, NodeKeyConfig}; +use sc_service::Role; +use sp_core::H256; +use std::{path::PathBuf, str::FromStr}; + +use crate::{arg_enums::NodeKeyType, error, Error}; + +/// The file name of the node's Ed25519 secret key inside the chain-specific +/// network config directory, if neither `--node-key` nor `--node-key-file` +/// is specified in combination with `--node-key-type=ed25519`. +pub(crate) const NODE_KEY_ED25519_FILE: &str = "secret_ed25519"; + +/// Parameters used to create the `NodeKeyConfig`, which determines the keypair +/// used for libp2p networking. +#[derive(Debug, Clone, Args)] +pub struct NodeKeyParams { + /// Secret key to use for p2p networking. + /// + /// The value is a string that is parsed according to the choice of + /// `--node-key-type` as follows: + /// + /// - `ed25519`: the value is parsed as a hex-encoded Ed25519 32 byte secret key (64 hex + /// chars) + /// + /// The value of this option takes precedence over `--node-key-file`. + /// + /// WARNING: Secrets provided as command-line arguments are easily exposed. + /// Use of this option should be limited to development and testing. To use + /// an externally managed secret key, use `--node-key-file` instead. + #[arg(long, value_name = "KEY")] + pub node_key: Option, + + /// Crypto primitive to use for p2p networking. + /// + /// The secret key of the node is obtained as follows: + /// + /// - If the `--node-key` option is given, the value is parsed as a secret key according to the + /// type. See the documentation for `--node-key`. + /// + /// - If the `--node-key-file` option is given, the secret key is read from the specified file. + /// See the documentation for `--node-key-file`. + /// + /// - Otherwise, the secret key is read from a file with a predetermined, type-specific name + /// from the chain-specific network config directory inside the base directory specified by + /// `--base-dir`. If this file does not exist, it is created with a newly generated secret + /// key of the chosen type. + /// + /// The node's secret key determines the corresponding public key and hence the + /// node's peer ID in the context of libp2p. + #[arg(long, value_name = "TYPE", value_enum, ignore_case = true, default_value_t = NodeKeyType::Ed25519)] + pub node_key_type: NodeKeyType, + + /// File from which to read the node's secret key to use for p2p networking. + /// + /// The contents of the file are parsed according to the choice of `--node-key-type` + /// as follows: + /// + /// - `ed25519`: the file must contain an unencoded 32 byte or hex encoded Ed25519 secret key. + /// + /// If the file does not exist, it is created with a newly generated secret key of + /// the chosen type. + #[arg(long, value_name = "FILE")] + pub node_key_file: Option, + + /// Forces key generation if node-key-file file does not exist. + /// + /// This is an unsafe feature for production networks, because as an active authority + /// other authorities may depend on your node having a stable identity and they might + /// not being able to reach you if your identity changes after entering the active set. + /// + /// For minimal node downtime if no custom `node-key-file` argument is provided + /// the network-key is usually persisted across nodes restarts, + /// in the `network` folder from directory provided in `--base-path` + /// + /// Warning!! If you ever run the node with this argument, make sure + /// you remove it for the subsequent restarts. + #[arg(long)] + pub unsafe_force_node_key_generation: bool, +} + +impl NodeKeyParams { + /// Create a `NodeKeyConfig` from the given `NodeKeyParams` in the context + /// of an optional network config storage directory. + pub fn node_key( + &self, + net_config_dir: &PathBuf, + role: Role, + is_dev: bool, + ) -> error::Result { + Ok(match self.node_key_type { + NodeKeyType::Ed25519 => { + let secret = if let Some(node_key) = self.node_key.as_ref() { + parse_ed25519_secret(node_key)? + } else { + let key_path = self + .node_key_file + .clone() + .unwrap_or_else(|| net_config_dir.join(NODE_KEY_ED25519_FILE)); + if !self.unsafe_force_node_key_generation + && role.is_authority() + && !is_dev + && !key_path.exists() + { + return Err(Error::NetworkKeyNotFound(key_path)); + } + sc_network::config::Secret::File(key_path) + }; + + NodeKeyConfig::Ed25519(secret) + } + }) + } +} + +/// Create an error caused by an invalid node key argument. +fn invalid_node_key(e: impl std::fmt::Display) -> error::Error { + error::Error::Input(format!("Invalid node key: {}", e)) +} + +/// Parse a Ed25519 secret key from a hex string into a `sc_network::Secret`. +fn parse_ed25519_secret(hex: &str) -> error::Result { + H256::from_str(hex) + .map_err(invalid_node_key) + .and_then(|bytes| { + ed25519::SecretKey::try_from_bytes(bytes) + .map(sc_network::config::Secret::Input) + .map_err(invalid_node_key) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::ValueEnum; + use sc_network::config::ed25519; + use std::fs::{self, File}; + use tempfile::TempDir; + + #[test] + fn test_node_key_config_input() { + fn secret_input(net_config_dir: &PathBuf) -> error::Result<()> { + NodeKeyType::value_variants().iter().try_for_each(|t| { + let node_key_type = *t; + let sk = match node_key_type { + NodeKeyType::Ed25519 => ed25519::SecretKey::generate().as_ref().to_vec(), + }; + let params = NodeKeyParams { + node_key_type, + node_key: Some(format!("{:x}", H256::from_slice(sk.as_ref()))), + node_key_file: None, + unsafe_force_node_key_generation: false, + }; + params + .node_key(net_config_dir, Role::Authority, false) + .and_then(|c| match c { + NodeKeyConfig::Ed25519(sc_network::config::Secret::Input(ref ski)) + if node_key_type == NodeKeyType::Ed25519 && &sk[..] == ski.as_ref() => + { + Ok(()) + } + _ => Err(error::Error::Input("Unexpected node key config".into())), + }) + }) + } + + assert!(secret_input(&PathBuf::from_str("x").unwrap()).is_ok()); + } + + #[test] + fn test_node_key_config_file() { + fn check_key(file: PathBuf, key: &ed25519::SecretKey) { + let params = NodeKeyParams { + node_key_type: NodeKeyType::Ed25519, + node_key: None, + node_key_file: Some(file), + unsafe_force_node_key_generation: false, + }; + + let node_key = params + .node_key(&PathBuf::from("not-used"), Role::Authority, false) + .expect("Creates node key config") + .into_keypair() + .expect("Creates node key pair"); + + if node_key.secret().as_ref() != key.as_ref() { + panic!("Invalid key") + } + } + + let tmp = tempfile::Builder::new() + .prefix("alice") + .tempdir() + .expect("Creates tempfile"); + let file = tmp.path().join("mysecret").to_path_buf(); + let key = ed25519::SecretKey::generate(); + + fs::write(&file, array_bytes::bytes2hex("", key.as_ref())).expect("Writes secret key"); + check_key(file.clone(), &key); + + fs::write(&file, &key).expect("Writes secret key"); + check_key(file.clone(), &key); + } + + #[test] + fn test_node_key_config_default() { + fn with_def_params(f: F, unsafe_force_node_key_generation: bool) -> error::Result<()> + where + F: Fn(NodeKeyParams) -> error::Result<()>, + { + NodeKeyType::value_variants().iter().try_for_each(|t| { + let node_key_type = *t; + f(NodeKeyParams { + node_key_type, + node_key: None, + node_key_file: None, + unsafe_force_node_key_generation, + }) + }) + } + + fn some_config_dir( + net_config_dir: &PathBuf, + unsafe_force_node_key_generation: bool, + role: Role, + is_dev: bool, + ) -> error::Result<()> { + with_def_params( + |params| { + let dir = net_config_dir.clone(); + let key_type = params.node_key_type; + params + .node_key(net_config_dir, role, is_dev) + .and_then(move |c| match c { + NodeKeyConfig::Ed25519(sc_network::config::Secret::File(ref f)) + if key_type == NodeKeyType::Ed25519 + && f == &dir.join(NODE_KEY_ED25519_FILE) => + { + Ok(()) + } + _ => Err(error::Error::Input("Unexpected node key config".into())), + }) + }, + unsafe_force_node_key_generation, + ) + } + + assert!( + some_config_dir(&PathBuf::from_str("x").unwrap(), false, Role::Full, false).is_ok() + ); + assert!(some_config_dir( + &PathBuf::from_str("x").unwrap(), + false, + Role::Authority, + true + ) + .is_ok()); + assert!(some_config_dir( + &PathBuf::from_str("x").unwrap(), + true, + Role::Authority, + false + ) + .is_ok()); + assert!(matches!( + some_config_dir( + &PathBuf::from_str("x").unwrap(), + false, + Role::Authority, + false + ), + Err(Error::NetworkKeyNotFound(_)) + )); + + let tempdir = TempDir::new().unwrap(); + let _file = File::create(tempdir.path().join(NODE_KEY_ED25519_FILE)).unwrap(); + assert!(some_config_dir(&tempdir.path().into(), false, Role::Authority, false).is_ok()); + } +} diff --git a/substrate/cli/src/params/offchain_worker_params.rs b/substrate/cli/src/params/offchain_worker_params.rs new file mode 100644 index 00000000000..4aa49482663 --- /dev/null +++ b/substrate/cli/src/params/offchain_worker_params.rs @@ -0,0 +1,53 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Offchain worker related configuration parameters. +//! +//! A subset of configuration parameters which are relevant to +//! the inner working of offchain workers. The usage is solely +//! targeted at handling input parameter parsing providing +//! a reasonable abstraction. + +use clap::{ArgAction, Args}; +use sc_network::config::Role; +use sc_service::config::OffchainWorkerConfig; + +use crate::{error, OffchainWorkerEnabled}; + +/// Offchain worker related parameters. +#[derive(Debug, Clone, Args)] +pub struct OffchainWorkerParams { + /// Execute offchain workers on every block. + #[arg( + long = "offchain-worker", + value_name = "ENABLED", + value_enum, + ignore_case = true, + default_value_t = OffchainWorkerEnabled::WhenAuthority + )] + pub enabled: OffchainWorkerEnabled, + + /// Enable offchain indexing API. + /// + /// Allows the runtime to write directly to offchain workers DB during block import. + #[arg(long = "enable-offchain-indexing", value_name = "ENABLE_OFFCHAIN_INDEXING", default_value_t = false, action = ArgAction::Set)] + pub indexing_enabled: bool, +} + +impl OffchainWorkerParams { + /// Load spec to `Configuration` from `OffchainWorkerParams` and spec factory. + pub fn offchain_worker(&self, role: &Role) -> error::Result { + let enabled = match (&self.enabled, role) { + (OffchainWorkerEnabled::WhenAuthority, Role::Authority) => true, + (OffchainWorkerEnabled::Always, _) => true, + (OffchainWorkerEnabled::Never, _) => false, + (OffchainWorkerEnabled::WhenAuthority, _) => false, + }; + + let indexing_enabled = self.indexing_enabled; + Ok(OffchainWorkerConfig { + enabled, + indexing_enabled, + }) + } +} diff --git a/substrate/cli/src/params/prometheus_params.rs b/substrate/cli/src/params/prometheus_params.rs new file mode 100644 index 00000000000..515018a22e2 --- /dev/null +++ b/substrate/cli/src/params/prometheus_params.rs @@ -0,0 +1,51 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use clap::Args; +use sc_service::config::PrometheusConfig; +use std::net::{Ipv4Addr, SocketAddr}; + +/// Parameters used to config prometheus. +#[derive(Debug, Clone, Args)] +pub struct PrometheusParams { + /// Specify Prometheus exporter TCP Port. + #[arg(long, value_name = "PORT")] + pub prometheus_port: Option, + /// Expose Prometheus exporter on all interfaces. + /// + /// Default is local. + #[arg(long)] + pub prometheus_external: bool, + /// Do not expose a Prometheus exporter endpoint. + /// + /// Prometheus metric endpoint is enabled by default. + #[arg(long)] + pub no_prometheus: bool, +} + +impl PrometheusParams { + /// Creates [`PrometheusConfig`]. + pub fn prometheus_config( + &self, + default_listen_port: u16, + chain_id: String, + ) -> Option { + if self.no_prometheus { + None + } else { + let interface = if self.prometheus_external { + Ipv4Addr::UNSPECIFIED + } else { + Ipv4Addr::LOCALHOST + }; + + Some(PrometheusConfig::new_with_default_registry( + SocketAddr::new( + interface.into(), + self.prometheus_port.unwrap_or(default_listen_port), + ), + chain_id, + )) + } + } +} diff --git a/substrate/cli/src/params/pruning_params.rs b/substrate/cli/src/params/pruning_params.rs new file mode 100644 index 00000000000..26732e8e32c --- /dev/null +++ b/substrate/cli/src/params/pruning_params.rs @@ -0,0 +1,168 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::error; +use clap::Args; +use sc_service::{BlocksPruning, PruningMode}; + +/// Parameters to define the pruning mode +#[derive(Debug, Clone, Args)] +pub struct PruningParams { + /// Specify the state pruning mode. + /// + /// This mode specifies when the block's state (ie, storage) + /// should be pruned (ie, removed) from the database. + /// This setting can only be set on the first creation of the database. Every subsequent run + /// will load the pruning mode from the database and will error if the stored mode doesn't + /// match this CLI value. It is fine to drop this CLI flag for subsequent runs. The only + /// exception is that `NUMBER` can change between subsequent runs (increasing it will not + /// lead to restoring pruned state). + /// + /// Possible values: + /// + /// - archive: Keep the data of all blocks. + /// + /// - archive-canonical: Keep only the data of finalized blocks. + /// + /// - NUMBER: Keep the data of the last NUMBER of finalized blocks. + /// + /// [default: 256] + #[arg(alias = "pruning", long, value_name = "PRUNING_MODE")] + pub state_pruning: Option, + + /// Specify the blocks pruning mode. + /// + /// This mode specifies when the block's body (including justifications) + /// should be pruned (ie, removed) from the database. + /// + /// Possible values: + /// + /// - archive: Keep the data of all blocks. + /// + /// - archive-canonical: Keep only the data of finalized blocks. + /// + /// - NUMBER: Keep the data of the last NUMBER of finalized blocks. + #[arg( + alias = "keep-blocks", + long, + value_name = "PRUNING_MODE", + default_value = "archive-canonical" + )] + pub blocks_pruning: DatabasePruningMode, +} + +impl PruningParams { + /// Get the pruning value from the parameters + pub fn state_pruning(&self) -> error::Result> { + Ok(self.state_pruning.map(|v| v.into())) + } + + /// Get the block pruning value from the parameters + pub fn blocks_pruning(&self) -> error::Result { + Ok(self.blocks_pruning.into()) + } +} + +/// Specifies the pruning mode of the database. +/// +/// This specifies when the block's data (either state via `--state-pruning` +/// or body via `--blocks-pruning`) should be pruned (ie, removed) from +/// the database. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum DatabasePruningMode { + /// Keep the data of all blocks. + Archive, + /// Keep only the data of finalized blocks. + ArchiveCanonical, + /// Keep the data of the last number of finalized blocks. + Custom(u32), +} + +impl std::str::FromStr for DatabasePruningMode { + type Err = String; + + fn from_str(input: &str) -> Result { + match input { + "archive" => Ok(Self::Archive), + "archive-canonical" => Ok(Self::ArchiveCanonical), + bc => bc + .parse() + .map_err(|_| "Invalid pruning mode specified".to_string()) + .map(Self::Custom), + } + } +} + +impl From for PruningMode { + fn from(val: DatabasePruningMode) -> Self { + match val { + DatabasePruningMode::Archive => PruningMode::ArchiveAll, + DatabasePruningMode::ArchiveCanonical => PruningMode::ArchiveCanonical, + DatabasePruningMode::Custom(n) => PruningMode::blocks_pruning(n), + } + } +} + +impl From for BlocksPruning { + fn from(val: DatabasePruningMode) -> Self { + match val { + DatabasePruningMode::Archive => BlocksPruning::KeepAll, + DatabasePruningMode::ArchiveCanonical => BlocksPruning::KeepFinalized, + DatabasePruningMode::Custom(n) => BlocksPruning::Some(n), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::Parser; + + #[derive(Parser)] + struct Cli { + #[clap(flatten)] + pruning: PruningParams, + } + + #[test] + fn pruning_params_parse_works() { + let Cli { pruning } = + Cli::parse_from(["", "--state-pruning=1000", "--blocks-pruning=1000"]); + + assert!(matches!( + pruning.state_pruning, + Some(DatabasePruningMode::Custom(1000)) + )); + assert!(matches!( + pruning.blocks_pruning, + DatabasePruningMode::Custom(1000) + )); + + let Cli { pruning } = + Cli::parse_from(["", "--state-pruning=archive", "--blocks-pruning=archive"]); + + assert!(matches!( + dbg!(pruning.state_pruning), + Some(DatabasePruningMode::Archive) + )); + assert!(matches!( + pruning.blocks_pruning, + DatabasePruningMode::Archive + )); + + let Cli { pruning } = Cli::parse_from([ + "", + "--state-pruning=archive-canonical", + "--blocks-pruning=archive-canonical", + ]); + + assert!(matches!( + dbg!(pruning.state_pruning), + Some(DatabasePruningMode::ArchiveCanonical) + )); + assert!(matches!( + pruning.blocks_pruning, + DatabasePruningMode::ArchiveCanonical + )); + } +} diff --git a/substrate/cli/src/params/rpc_params.rs b/substrate/cli/src/params/rpc_params.rs new file mode 100644 index 00000000000..eae643d3d55 --- /dev/null +++ b/substrate/cli/src/params/rpc_params.rs @@ -0,0 +1,395 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::{ + arg_enums::RpcMethods, + params::{IpNetwork, RpcBatchRequestConfig}, + RPC_DEFAULT_MAX_CONNECTIONS, RPC_DEFAULT_MAX_REQUEST_SIZE_MB, RPC_DEFAULT_MAX_RESPONSE_SIZE_MB, + RPC_DEFAULT_MAX_SUBS_PER_CONN, RPC_DEFAULT_MESSAGE_CAPACITY_PER_CONN, +}; +use std::{net::SocketAddr, num::NonZeroU32}; + +const RPC_LISTEN_ADDR: &str = "listen-addr"; +const RPC_CORS: &str = "cors"; +const RPC_MAX_CONNS: &str = "max-connections"; +const RPC_MAX_REQUEST_SIZE: &str = "max-request-size"; +const RPC_MAX_RESPONSE_SIZE: &str = "max-response-size"; +const RPC_MAX_SUBS_PER_CONN: &str = "max-subscriptions-per-connection"; +const RPC_MAX_BUF_CAP_PER_CONN: &str = "max-buffer-capacity-per-connection"; +const RPC_RATE_LIMIT: &str = "rate-limit"; +const RPC_RATE_LIMIT_TRUST_PROXY_HEADERS: &str = "rate-limit-trust-proxy-headers"; +const RPC_RATE_LIMIT_WHITELISTED_IPS: &str = "rate-limit-whitelisted-ips"; +const RPC_RETRY_RANDOM_PORT: &str = "retry-random-port"; +const RPC_METHODS: &str = "methods"; +const RPC_OPTIONAL: &str = "optional"; +const RPC_DISABLE_BATCH: &str = "disable-batch-requests"; +const RPC_BATCH_LIMIT: &str = "max-batch-request-len"; + +/// Represent a single RPC endpoint with its configuration. +#[derive(Debug, Clone)] +pub struct RpcEndpoint { + /// Listen address. + pub listen_addr: SocketAddr, + /// Batch request configuration. + pub batch_config: RpcBatchRequestConfig, + /// Maximum number of connections. + pub max_connections: u32, + /// Maximum inbound payload size in MB. + pub max_payload_in_mb: u32, + /// Maximum outbound payload size in MB. + pub max_payload_out_mb: u32, + /// Maximum number of subscriptions per connection. + pub max_subscriptions_per_connection: u32, + /// Maximum buffer capacity per connection. + pub max_buffer_capacity_per_connection: u32, + /// Rate limit per minute. + pub rate_limit: Option, + /// Whether to trust proxy headers for rate limiting. + pub rate_limit_trust_proxy_headers: bool, + /// Whitelisted IPs for rate limiting. + pub rate_limit_whitelisted_ips: Vec, + /// CORS. + pub cors: Option>, + /// RPC methods to expose. + pub rpc_methods: RpcMethods, + /// Whether it's an optional listening address i.e, it's ignored if it fails to bind. + /// For example substrate tries to bind both ipv4 and ipv6 addresses but some platforms + /// may not support ipv6. + pub is_optional: bool, + /// Whether to retry with a random port if the provided port is already in use. + pub retry_random_port: bool, +} + +impl std::str::FromStr for RpcEndpoint { + type Err = String; + + fn from_str(s: &str) -> Result { + let mut listen_addr = None; + let mut max_connections = None; + let mut max_payload_in_mb = None; + let mut max_payload_out_mb = None; + let mut max_subscriptions_per_connection = None; + let mut max_buffer_capacity_per_connection = None; + let mut cors: Option> = None; + let mut rpc_methods = None; + let mut is_optional = None; + let mut disable_batch_requests = None; + let mut max_batch_request_len = None; + let mut rate_limit = None; + let mut rate_limit_trust_proxy_headers = None; + let mut rate_limit_whitelisted_ips = Vec::new(); + let mut retry_random_port = None; + + for input in s.split(',') { + let (key, val) = input + .trim() + .split_once('=') + .ok_or_else(|| invalid_input(input))?; + let key = key.trim(); + let val = val.trim(); + + match key { + RPC_LISTEN_ADDR => { + if listen_addr.is_some() { + return Err(only_once_err(RPC_LISTEN_ADDR)); + } + let val: SocketAddr = val + .parse() + .map_err(|_| invalid_value(RPC_LISTEN_ADDR, val))?; + listen_addr = Some(val); + } + RPC_CORS => { + if val.is_empty() { + return Err(invalid_value(RPC_CORS, val)); + } + + if let Some(cors) = cors.as_mut() { + cors.push(val.to_string()); + } else { + cors = Some(vec![val.to_string()]); + } + } + RPC_MAX_CONNS => { + if max_connections.is_some() { + return Err(only_once_err(RPC_MAX_CONNS)); + } + + let val = val.parse().map_err(|_| invalid_value(RPC_MAX_CONNS, val))?; + max_connections = Some(val); + } + RPC_MAX_REQUEST_SIZE => { + if max_payload_in_mb.is_some() { + return Err(only_once_err(RPC_MAX_REQUEST_SIZE)); + } + + let val = val + .parse() + .map_err(|_| invalid_value(RPC_MAX_RESPONSE_SIZE, val))?; + max_payload_in_mb = Some(val); + } + RPC_MAX_RESPONSE_SIZE => { + if max_payload_out_mb.is_some() { + return Err(only_once_err(RPC_MAX_RESPONSE_SIZE)); + } + + let val = val + .parse() + .map_err(|_| invalid_value(RPC_MAX_RESPONSE_SIZE, val))?; + max_payload_out_mb = Some(val); + } + RPC_MAX_SUBS_PER_CONN => { + if max_subscriptions_per_connection.is_some() { + return Err(only_once_err(RPC_MAX_SUBS_PER_CONN)); + } + + let val = val + .parse() + .map_err(|_| invalid_value(RPC_MAX_SUBS_PER_CONN, val))?; + max_subscriptions_per_connection = Some(val); + } + RPC_MAX_BUF_CAP_PER_CONN => { + if max_buffer_capacity_per_connection.is_some() { + return Err(only_once_err(RPC_MAX_BUF_CAP_PER_CONN)); + } + + let val = val + .parse() + .map_err(|_| invalid_value(RPC_MAX_BUF_CAP_PER_CONN, val))?; + max_buffer_capacity_per_connection = Some(val); + } + RPC_RATE_LIMIT => { + if rate_limit.is_some() { + return Err(only_once_err("rate-limit")); + } + + let val = val + .parse() + .map_err(|_| invalid_value(RPC_RATE_LIMIT, val))?; + rate_limit = Some(val); + } + RPC_RATE_LIMIT_TRUST_PROXY_HEADERS => { + if rate_limit_trust_proxy_headers.is_some() { + return Err(only_once_err(RPC_RATE_LIMIT_TRUST_PROXY_HEADERS)); + } + + let val = val + .parse() + .map_err(|_| invalid_value(RPC_RATE_LIMIT_TRUST_PROXY_HEADERS, val))?; + rate_limit_trust_proxy_headers = Some(val); + } + RPC_RATE_LIMIT_WHITELISTED_IPS => { + let ip: IpNetwork = val + .parse() + .map_err(|_| invalid_value(RPC_RATE_LIMIT_WHITELISTED_IPS, val))?; + rate_limit_whitelisted_ips.push(ip); + } + RPC_RETRY_RANDOM_PORT => { + if retry_random_port.is_some() { + return Err(only_once_err(RPC_RETRY_RANDOM_PORT)); + } + let val = val + .parse() + .map_err(|_| invalid_value(RPC_RETRY_RANDOM_PORT, val))?; + retry_random_port = Some(val); + } + RPC_METHODS => { + if rpc_methods.is_some() { + return Err(only_once_err("methods")); + } + let val = val.parse().map_err(|_| invalid_value(RPC_METHODS, val))?; + rpc_methods = Some(val); + } + RPC_OPTIONAL => { + if is_optional.is_some() { + return Err(only_once_err(RPC_OPTIONAL)); + } + + let val = val.parse().map_err(|_| invalid_value(RPC_OPTIONAL, val))?; + is_optional = Some(val); + } + RPC_DISABLE_BATCH => { + if disable_batch_requests.is_some() { + return Err(only_once_err(RPC_DISABLE_BATCH)); + } + + let val = val + .parse() + .map_err(|_| invalid_value(RPC_DISABLE_BATCH, val))?; + disable_batch_requests = Some(val); + } + RPC_BATCH_LIMIT => { + if max_batch_request_len.is_some() { + return Err(only_once_err(RPC_BATCH_LIMIT)); + } + + let val = val + .parse() + .map_err(|_| invalid_value(RPC_BATCH_LIMIT, val))?; + max_batch_request_len = Some(val); + } + _ => return Err(invalid_key(key)), + } + } + + let listen_addr = listen_addr.ok_or("`listen-addr` must be specified exactly once")?; + + let batch_config = match (disable_batch_requests, max_batch_request_len) { + (Some(true), Some(_)) => { + return Err(format!("`{RPC_BATCH_LIMIT}` and `{RPC_DISABLE_BATCH}` are mutually exclusive and can't be used together")); + } + (Some(false), None) => RpcBatchRequestConfig::Disabled, + (None, Some(len)) => RpcBatchRequestConfig::Limit(len), + _ => RpcBatchRequestConfig::Unlimited, + }; + + Ok(Self { + listen_addr, + batch_config, + max_connections: max_connections.unwrap_or(RPC_DEFAULT_MAX_CONNECTIONS), + max_payload_in_mb: max_payload_in_mb.unwrap_or(RPC_DEFAULT_MAX_REQUEST_SIZE_MB), + max_payload_out_mb: max_payload_out_mb.unwrap_or(RPC_DEFAULT_MAX_RESPONSE_SIZE_MB), + cors, + max_buffer_capacity_per_connection: max_buffer_capacity_per_connection + .unwrap_or(RPC_DEFAULT_MESSAGE_CAPACITY_PER_CONN), + max_subscriptions_per_connection: max_subscriptions_per_connection + .unwrap_or(RPC_DEFAULT_MAX_SUBS_PER_CONN), + rpc_methods: rpc_methods.unwrap_or(RpcMethods::Auto), + rate_limit, + rate_limit_trust_proxy_headers: rate_limit_trust_proxy_headers.unwrap_or(false), + rate_limit_whitelisted_ips, + is_optional: is_optional.unwrap_or(false), + retry_random_port: retry_random_port.unwrap_or(false), + }) + } +} + +impl From for sc_service::config::RpcEndpoint { + fn from(val: RpcEndpoint) -> Self { + sc_service::config::RpcEndpoint { + batch_config: val.batch_config, + listen_addr: val.listen_addr, + max_buffer_capacity_per_connection: val.max_buffer_capacity_per_connection, + max_connections: val.max_connections, + max_payload_in_mb: val.max_payload_in_mb, + max_payload_out_mb: val.max_payload_out_mb, + max_subscriptions_per_connection: val.max_subscriptions_per_connection, + rpc_methods: val.rpc_methods.into(), + rate_limit: val.rate_limit, + rate_limit_trust_proxy_headers: val.rate_limit_trust_proxy_headers, + rate_limit_whitelisted_ips: val.rate_limit_whitelisted_ips, + cors: val.cors, + retry_random_port: val.retry_random_port, + is_optional: val.is_optional, + } + } +} + +impl RpcEndpoint { + /// Returns whether the endpoint is globally exposed. + pub fn is_global(&self) -> bool { + let ip = IpNetwork::from(self.listen_addr.ip()); + ip.is_global() + } +} + +fn only_once_err(reason: &str) -> String { + format!("`{reason}` is only allowed be specified once") +} + +fn invalid_input(input: &str) -> String { + format!("`{input}`, expects: `key=value`") +} + +fn invalid_value(key: &str, value: &str) -> String { + format!("value=`{value}` key=`{key}`") +} + +fn invalid_key(key: &str) -> String { + format!("unknown key=`{key}`, see `--help` for available options") +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{num::NonZeroU32, str::FromStr}; + + #[test] + fn parse_rpc_endpoint_works() { + assert!(RpcEndpoint::from_str("listen-addr=127.0.0.1:9944").is_ok()); + assert!(RpcEndpoint::from_str("listen-addr=[::1]:9944").is_ok()); + assert!(RpcEndpoint::from_str("listen-addr=127.0.0.1:9944,methods=auto").is_ok()); + assert!(RpcEndpoint::from_str("listen-addr=[::1]:9944,methods=auto").is_ok()); + assert!(RpcEndpoint::from_str( + "listen-addr=127.0.0.1:9944,methods=auto,cors=*,optional=true" + ) + .is_ok()); + + assert!(RpcEndpoint::from_str("listen-addrs=127.0.0.1:9944,foo=*").is_err()); + assert!(RpcEndpoint::from_str("listen-addrs=127.0.0.1:9944,cors=").is_err()); + } + + #[test] + fn parse_rpc_endpoint_all() { + let endpoint = RpcEndpoint::from_str( + "listen-addr=127.0.0.1:9944,methods=unsafe,cors=*,optional=true,retry-random-port=true,rate-limit=99,\ + max-batch-request-len=100,rate-limit-trust-proxy-headers=true,max-connections=33,max-request-size=4,\ + max-response-size=3,max-subscriptions-per-connection=7,max-buffer-capacity-per-connection=8,\ + rate-limit-whitelisted-ips=192.168.1.0/24,rate-limit-whitelisted-ips=ff01::0/32" + ).unwrap(); + assert_eq!(endpoint.listen_addr, ([127, 0, 0, 1], 9944).into()); + assert_eq!(endpoint.rpc_methods, RpcMethods::Unsafe); + assert_eq!(endpoint.cors, Some(vec!["*".to_string()])); + assert!(endpoint.is_optional); + assert!(endpoint.retry_random_port); + assert_eq!(endpoint.rate_limit, Some(NonZeroU32::new(99).unwrap())); + assert!(matches!(endpoint.batch_config, RpcBatchRequestConfig::Limit(l) if l == 100)); + assert!(endpoint.rate_limit_trust_proxy_headers); + assert_eq!( + endpoint.rate_limit_whitelisted_ips, + vec![ + IpNetwork::V4("192.168.1.0/24".parse().unwrap()), + IpNetwork::V6("ff01::0/32".parse().unwrap()) + ] + ); + assert_eq!(endpoint.max_connections, 33); + assert_eq!(endpoint.max_payload_in_mb, 4); + assert_eq!(endpoint.max_payload_out_mb, 3); + assert_eq!(endpoint.max_subscriptions_per_connection, 7); + assert_eq!(endpoint.max_buffer_capacity_per_connection, 8); + } + + #[test] + fn parse_rpc_endpoint_multiple_cors() { + let addr = RpcEndpoint::from_str( + "listen-addr=127.0.0.1:9944,methods=auto,cors=https://polkadot.js.org,cors=*,cors=localhost:*", + ) + .unwrap(); + + assert_eq!( + addr.cors, + Some(vec![ + "https://polkadot.js.org".to_string(), + "*".to_string(), + "localhost:*".to_string() + ]) + ); + } + + #[test] + fn parse_rpc_endpoint_whitespaces() { + let addr = RpcEndpoint::from_str( + " listen-addr = 127.0.0.1:9944, methods = auto, optional = true ", + ) + .unwrap(); + assert_eq!(addr.rpc_methods, RpcMethods::Auto); + assert!(addr.is_optional); + } + + #[test] + fn parse_rpc_endpoint_batch_options_mutually_exclusive() { + assert!(RpcEndpoint::from_str( + "listen-addr = 127.0.0.1:9944,disable-batch-requests=true,max-batch-request-len=100", + ) + .is_err()); + } +} diff --git a/substrate/cli/src/params/runtime_params.rs b/substrate/cli/src/params/runtime_params.rs new file mode 100644 index 00000000000..c5063f43339 --- /dev/null +++ b/substrate/cli/src/params/runtime_params.rs @@ -0,0 +1,30 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use clap::Args; +use std::str::FromStr; + +/// Parameters used to config runtime. +#[derive(Debug, Clone, Args)] +pub struct RuntimeParams { + /// The size of the instances cache for each runtime [max: 32]. + /// + /// Values higher than 32 are illegal. + #[arg(long, default_value_t = 8, value_parser = parse_max_runtime_instances)] + pub max_runtime_instances: usize, + + /// Maximum number of different runtimes that can be cached. + #[arg(long, default_value_t = 2)] + pub runtime_cache_size: u8, +} + +fn parse_max_runtime_instances(s: &str) -> Result { + let max_runtime_instances = usize::from_str(s) + .map_err(|_err| format!("Illegal `--max-runtime-instances` value: {s}"))?; + + if max_runtime_instances > 32 { + Err(format!("Illegal `--max-runtime-instances` value: {max_runtime_instances} is more than the allowed maximum of `32` ")) + } else { + Ok(max_runtime_instances) + } +} diff --git a/substrate/cli/src/params/shared_params.rs b/substrate/cli/src/params/shared_params.rs new file mode 100644 index 00000000000..cad2e1ebc4c --- /dev/null +++ b/substrate/cli/src/params/shared_params.rs @@ -0,0 +1,136 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::arg_enums::TracingReceiver; +use clap::Args; +use sc_service::config::BasePath; +use std::path::PathBuf; + +/// Shared parameters used by all `CoreParams`. +#[derive(Debug, Clone, Args)] +pub struct SharedParams { + /// Specify the chain specification. + /// + /// It can be one of the predefined ones (dev, local, or staging) or it can be a path to + /// a file with the chainspec (such as one exported by the `build-spec` subcommand). + #[arg(long, value_name = "CHAIN_SPEC")] + pub chain: Option, + + /// Specify the development chain. + /// + /// This flag sets `--chain=dev`, `--force-authoring`, `--rpc-cors=all`, + /// `--alice`, and `--tmp` flags, unless explicitly overridden. + /// It also disables local peer discovery (see --no-mdns and --discover-local) + #[arg(long, conflicts_with_all = &["chain"])] + pub dev: bool, + + /// Specify custom base path. + #[arg(long, short = 'd', value_name = "PATH")] + pub base_path: Option, + + /// Sets a custom logging filter (syntax: `=`). + /// + /// Log levels (least to most verbose) are `error`, `warn`, `info`, `debug`, and `trace`. + /// + /// By default, all targets log `info`. The global log level can be set with `-l`. + /// + /// Multiple `=` entries can be specified and separated by a comma. + /// + /// *Example*: `--log error,sync=debug,grandpa=warn`. + /// Sets Global log level to `error`, sets `sync` target to debug and grandpa target to `warn`. + #[arg(short = 'l', long, value_name = "LOG_PATTERN", num_args = 1..)] + pub log: Vec, + + /// Enable detailed log output. + /// + /// Includes displaying the log target, log level and thread name. + /// + /// This is automatically enabled when something is logged with any higher level than `info`. + #[arg(long)] + pub detailed_log_output: bool, + + /// Disable log color output. + #[arg(long)] + pub disable_log_color: bool, + + /// Enable feature to dynamically update and reload the log filter. + /// + /// Be aware that enabling this feature can lead to a performance decrease up to factor six or + /// more. Depending on the global logging level the performance decrease changes. + /// + /// The `system_addLogFilter` and `system_resetLogFilter` RPCs will have no effect with this + /// option not being set. + #[arg(long)] + pub enable_log_reloading: bool, + + /// Sets a custom profiling filter. + /// + /// Syntax is the same as for logging (`--log`). + #[arg(long, value_name = "TARGETS")] + pub tracing_targets: Option, + + /// Receiver to process tracing messages. + #[arg(long, value_name = "RECEIVER", value_enum, ignore_case = true, default_value_t = TracingReceiver::Log)] + pub tracing_receiver: TracingReceiver, +} + +impl SharedParams { + /// Specify custom base path. + pub fn base_path(&self) -> Result, crate::Error> { + match &self.base_path { + Some(r) => Ok(Some(r.clone().into())), + // If `dev` is enabled, we use the temp base path. + None if self.is_dev() => Ok(Some(BasePath::new_temp_dir()?)), + None => Ok(None), + } + } + + /// Specify the development chain. + pub fn is_dev(&self) -> bool { + self.dev + } + + /// Get the chain spec for the parameters provided + pub fn chain_id(&self, is_dev: bool) -> String { + match self.chain { + Some(ref chain) => chain.clone(), + None => { + if is_dev { + "dev".into() + } else { + "".into() + } + } + } + } + + /// Get the filters for the logging + pub fn log_filters(&self) -> &[String] { + &self.log + } + + /// Should the detailed log output be enabled. + pub fn detailed_log_output(&self) -> bool { + self.detailed_log_output + } + + /// Should the log color output be disabled? + pub fn disable_log_color(&self) -> bool { + self.disable_log_color + } + + /// Is log reloading enabled + pub fn enable_log_reloading(&self) -> bool { + self.enable_log_reloading + } + + /// Receiver to process tracing messages. + pub fn tracing_receiver(&self) -> sc_service::TracingReceiver { + self.tracing_receiver.into() + } + + /// Comma separated list of targets for tracing. + pub fn tracing_targets(&self) -> Option { + self.tracing_targets.clone() + } +} diff --git a/substrate/cli/src/params/telemetry_params.rs b/substrate/cli/src/params/telemetry_params.rs new file mode 100644 index 00000000000..ebf1d908b84 --- /dev/null +++ b/substrate/cli/src/params/telemetry_params.rs @@ -0,0 +1,55 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use clap::Args; + +/// Parameters used to config telemetry. +#[derive(Debug, Clone, Args)] +pub struct TelemetryParams { + /// Disable connecting to the Substrate telemetry server. + /// + /// Telemetry is on by default on global chains. + #[arg(long)] + pub no_telemetry: bool, + + /// The URL of the telemetry server to connect to. + /// + /// This flag can be passed multiple times as a means to specify multiple + /// telemetry endpoints. Verbosity levels range from 0-9, with 0 denoting + /// the least verbosity. + /// + /// Expected format is 'URL VERBOSITY', e.g. `--telemetry-url 'wss://foo/bar 0'`. + #[arg(long = "telemetry-url", value_name = "URL VERBOSITY", value_parser = parse_telemetry_endpoints)] + pub telemetry_endpoints: Vec<(String, u8)>, +} + +#[derive(Debug)] +enum TelemetryParsingError { + MissingVerbosity, + VerbosityParsingError(std::num::ParseIntError), +} + +impl std::error::Error for TelemetryParsingError {} + +impl std::fmt::Display for TelemetryParsingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TelemetryParsingError::MissingVerbosity => write!(f, "Verbosity level missing"), + TelemetryParsingError::VerbosityParsingError(e) => write!(f, "{}", e), + } + } +} + +fn parse_telemetry_endpoints(s: &str) -> Result<(String, u8), TelemetryParsingError> { + let pos = s.find(' '); + match pos { + None => Err(TelemetryParsingError::MissingVerbosity), + Some(pos_) => { + let url = s[..pos_].to_string(); + let verbosity = s[pos_ + 1..] + .parse() + .map_err(TelemetryParsingError::VerbosityParsingError)?; + Ok((url, verbosity)) + } + } +} diff --git a/substrate/cli/src/params/transaction_pool_params.rs b/substrate/cli/src/params/transaction_pool_params.rs new file mode 100644 index 00000000000..a9839ed8f69 --- /dev/null +++ b/substrate/cli/src/params/transaction_pool_params.rs @@ -0,0 +1,49 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use clap::Args; +use sc_service::config::TransactionPoolOptions; + +/// Parameters used to create the pool configuration. +#[derive(Debug, Clone, Args)] +pub struct TransactionPoolParams { + /// Maximum number of transactions in the transaction pool. + #[arg(long, value_name = "COUNT", default_value_t = 8192)] + pub pool_limit: usize, + + /// Maximum number of kilobytes of all transactions stored in the pool. + #[arg(long, value_name = "COUNT", default_value_t = 20480)] + pub pool_kbytes: usize, + + /// How long a transaction is banned for. + /// + /// If it is considered invalid. Defaults to 1800s. + #[arg(long, value_name = "SECONDS")] + pub tx_ban_seconds: Option, +} + +impl TransactionPoolParams { + /// Fill the given `PoolConfiguration` by looking at the cli parameters. + pub fn transaction_pool(&self, is_dev: bool) -> TransactionPoolOptions { + let mut opts = TransactionPoolOptions::default(); + + // ready queue + opts.ready.count = self.pool_limit; + opts.ready.total_bytes = self.pool_kbytes * 1024; + + // future queue + let factor = 10; + opts.future.count = self.pool_limit / factor; + opts.future.total_bytes = self.pool_kbytes * 1024 / factor; + + opts.ban_time = if let Some(ban_seconds) = self.tx_ban_seconds { + std::time::Duration::from_secs(ban_seconds) + } else if is_dev { + std::time::Duration::from_secs(0) + } else { + std::time::Duration::from_secs(30 * 60) + }; + + opts + } +} diff --git a/substrate/cli/src/runner.rs b/substrate/cli/src/runner.rs new file mode 100644 index 00000000000..744347ced88 --- /dev/null +++ b/substrate/cli/src/runner.rs @@ -0,0 +1,425 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::{error::Error as CliError, Result, Signals, SubstrateCli}; +use chrono::prelude::*; +use futures::{future::FutureExt, Future}; +use log::info; +use sc_service::{Configuration, Error as ServiceError, TaskManager}; +use sc_utils::metrics::{TOKIO_THREADS_ALIVE, TOKIO_THREADS_TOTAL}; +use std::{marker::PhantomData, time::Duration}; + +/// Build a tokio runtime with all features. +pub fn build_runtime() -> std::result::Result { + tokio::runtime::Builder::new_multi_thread() + .on_thread_start(|| { + TOKIO_THREADS_ALIVE.inc(); + TOKIO_THREADS_TOTAL.inc(); + }) + .on_thread_stop(|| { + TOKIO_THREADS_ALIVE.dec(); + }) + .enable_all() + .build() +} + +/// A Substrate CLI runtime that can be used to run a node or a command +pub struct Runner { + config: Configuration, + tokio_runtime: tokio::runtime::Runtime, + signals: Signals, + phantom: PhantomData, +} + +impl Runner { + /// Create a new runtime with the command provided in argument + pub fn new( + config: Configuration, + tokio_runtime: tokio::runtime::Runtime, + signals: Signals, + ) -> Result> { + Ok(Runner { + config, + tokio_runtime, + signals, + phantom: PhantomData, + }) + } + + /// Log information about the node itself. + /// + /// # Example: + /// + /// ```text + /// 2020-06-03 16:14:21 Substrate Node + /// 2020-06-03 16:14:21 ✌️ version 2.0.0-rc3-f4940588c-x86_64-linux-gnu + /// 2020-06-03 16:14:21 ❤️ by Parity Technologies , 2017-2020 + /// 2020-06-03 16:14:21 📋 Chain specification: Flaming Fir + /// 2020-06-03 16:14:21 🏷 Node name: jolly-rod-7462 + /// 2020-06-03 16:14:21 👤 Role: FULL + /// 2020-06-03 16:14:21 💾 Database: RocksDb at /tmp/c/chains/flamingfir7/db + /// 2020-06-03 16:14:21 ⛓ Native runtime: node-251 (substrate-node-1.tx1.au10) + /// ``` + fn print_node_infos(&self) { + print_node_infos::(self.config()) + } + + /// A helper function that runs a node with tokio and stops if the process receives the signal + /// `SIGTERM` or `SIGINT`. + pub fn run_node_until_exit( + self, + initialize: impl FnOnce(Configuration) -> F, + ) -> std::result::Result<(), E> + where + F: Future>, + E: std::error::Error + Send + Sync + 'static + From, + { + self.print_node_infos(); + + let mut task_manager = self.tokio_runtime.block_on(initialize(self.config))?; + + let res = self + .tokio_runtime + .block_on(self.signals.run_until_signal(task_manager.future().fuse())); + // We need to drop the task manager here to inform all tasks that they should shut down. + // + // This is important to be done before we instruct the tokio runtime to shutdown. Otherwise + // the tokio runtime will wait the full 60 seconds for all tasks to stop. + let task_registry = task_manager.into_task_registry(); + + // Give all futures 60 seconds to shutdown, before tokio "leaks" them. + let shutdown_timeout = Duration::from_secs(60); + self.tokio_runtime.shutdown_timeout(shutdown_timeout); + + let running_tasks = task_registry.running_tasks(); + + if !running_tasks.is_empty() { + log::error!("Detected running(potentially stalled) tasks on shutdown:"); + running_tasks.iter().for_each(|(task, count)| { + let instances_desc = if *count > 1 { + format!("with {} instances ", count) + } else { + "".to_string() + }; + + if task.is_default_group() { + log::error!( + "Task \"{}\" was still running {}after waiting {} seconds to finish.", + task.name, + instances_desc, + shutdown_timeout.as_secs(), + ); + } else { + log::error!( + "Task \"{}\" (Group: {}) was still running {}after waiting {} seconds to finish.", + task.name, + task.group, + instances_desc, + shutdown_timeout.as_secs(), + ); + } + }); + } + + res.map_err(Into::into) + } + + /// A helper function that runs a command with the configuration of this node. + pub fn sync_run( + self, + runner: impl FnOnce(Configuration) -> std::result::Result<(), E>, + ) -> std::result::Result<(), E> + where + E: std::error::Error + Send + Sync + 'static + From, + { + runner(self.config) + } + + /// A helper function that runs a future with tokio and stops if the process receives + /// the signal `SIGTERM` or `SIGINT`. + pub fn async_run( + self, + runner: impl FnOnce(Configuration) -> std::result::Result<(F, TaskManager), E>, + ) -> std::result::Result<(), E> + where + F: Future>, + E: std::error::Error + Send + Sync + 'static + From + From, + { + let (future, task_manager) = runner(self.config)?; + self.tokio_runtime + .block_on(self.signals.run_until_signal(future.fuse()))?; + // Drop the task manager before dropping the rest, to ensure that all futures were informed + // about the shut down. + drop(task_manager); + Ok(()) + } + + /// Get an immutable reference to the node Configuration + pub fn config(&self) -> &Configuration { + &self.config + } + + /// Get a mutable reference to the node Configuration + pub fn config_mut(&mut self) -> &mut Configuration { + &mut self.config + } +} + +/// Log information about the node itself. +pub fn print_node_infos(config: &Configuration) { + info!("{}", C::impl_name()); + info!("✌️ version {}", C::impl_version()); + info!( + "❤️ by {}, {}-{}", + C::author(), + C::copyright_start_year(), + Local::now().year() + ); + info!("📋 Chain specification: {}", config.chain_spec.name()); + info!("🏷 Node name: {}", config.network.node_name); + info!("👤 Role: {}", config.display_role()); + info!( + "💾 Database: {} at {}", + config.database, + config + .database + .path() + .map_or_else(|| "".to_owned(), |p| p.display().to_string()) + ); +} + +#[cfg(test)] +mod tests { + use super::*; + use sc_network::config::NetworkConfiguration; + use sc_service::{ + config::{ExecutorConfiguration, RpcConfiguration}, + Arc, ChainType, GenericChainSpec, NoExtension, + }; + use std::{ + path::PathBuf, + sync::atomic::{AtomicU64, Ordering}, + }; + + struct Cli; + + impl SubstrateCli for Cli { + fn author() -> String { + "test".into() + } + + fn impl_name() -> String { + "yep".into() + } + + fn impl_version() -> String { + "version".into() + } + + fn description() -> String { + "desc".into() + } + + fn support_url() -> String { + "no.pe".into() + } + + fn copyright_start_year() -> i32 { + 2042 + } + + fn load_spec( + &self, + _: &str, + ) -> std::result::Result, String> { + Err("nope".into()) + } + } + + fn create_runner() -> Runner { + let runtime = build_runtime().unwrap(); + + let root = PathBuf::from("db"); + let runner = Runner::new( + Configuration { + impl_name: "spec".into(), + impl_version: "3".into(), + role: sc_service::Role::Authority, + tokio_handle: runtime.handle().clone(), + transaction_pool: Default::default(), + network: NetworkConfiguration::new_memory(), + keystore: sc_service::config::KeystoreConfig::InMemory, + database: sc_client_db::DatabaseSource::ParityDb { path: root.clone() }, + trie_cache_maximum_size: None, + state_pruning: None, + blocks_pruning: sc_client_db::BlocksPruning::KeepAll, + chain_spec: Box::new( + GenericChainSpec::::builder( + Default::default(), + NoExtension::None, + ) + .with_name("test") + .with_id("test_id") + .with_chain_type(ChainType::Development) + .with_genesis_config_patch(Default::default()) + .build(), + ), + executor: ExecutorConfiguration::default(), + wasm_runtime_overrides: None, + rpc: RpcConfiguration { + addr: None, + max_connections: Default::default(), + cors: None, + methods: Default::default(), + max_request_size: Default::default(), + max_response_size: Default::default(), + id_provider: Default::default(), + max_subs_per_conn: Default::default(), + message_buffer_capacity: Default::default(), + port: 9944, + batch_config: sc_service::config::RpcBatchRequestConfig::Unlimited, + method_limits: vec![], + rate_limit: None, + rate_limit_whitelisted_ips: Default::default(), + rate_limit_trust_proxy_headers: Default::default(), + }, + prometheus_config: None, + telemetry_endpoints: None, + offchain_worker: Default::default(), + force_authoring: false, + disable_grandpa: false, + dev_key_seed: None, + tracing_targets: None, + tracing_receiver: Default::default(), + announce_block: true, + base_path: sc_service::BasePath::new(root.clone()), + data_path: root, + }, + runtime, + Signals::dummy(), + ) + .unwrap(); + + runner + } + + #[test] + fn ensure_run_until_exit_informs_tasks_to_end() { + let runner = create_runner(); + + let counter = Arc::new(AtomicU64::new(0)); + let counter2 = counter.clone(); + + runner + .run_node_until_exit(move |cfg| async move { + let task_manager = TaskManager::new(cfg.tokio_handle.clone(), None).unwrap(); + let (sender, receiver) = futures::channel::oneshot::channel(); + + // We need to use `spawn_blocking` here so that we get a dedicated thread for our + // future. This is important for this test, as otherwise tokio can just "drop" the + // future. + task_manager + .spawn_handle() + .spawn_blocking("test", None, async move { + let _ = sender.send(()); + loop { + counter2.fetch_add(1, Ordering::Relaxed); + futures_timer::Delay::new(Duration::from_millis(50)).await; + } + }); + + task_manager + .spawn_essential_handle() + .spawn_blocking("test2", None, async { + // Let's stop this essential task directly when our other task started. + // It will signal that the task manager should end. + let _ = receiver.await; + }); + + Ok::<_, sc_service::Error>(task_manager) + }) + .unwrap_err(); + + let count = counter.load(Ordering::Relaxed); + + // Ensure that our counting task was running for less than 30 seconds. + // It should be directly killed, but for CI and whatever we are being a little bit more + // "relaxed". + assert!((count as u128) < (Duration::from_secs(30).as_millis() / 50)); + } + + fn run_test_in_another_process( + test_name: &str, + test_body: impl FnOnce(), + ) -> Option { + if std::env::var("RUN_FORKED_TEST").is_ok() { + test_body(); + None + } else { + let output = std::process::Command::new(std::env::current_exe().unwrap()) + .arg(test_name) + .env("RUN_FORKED_TEST", "1") + .env("RUST_LOG", "sc_cli=error") + .output() + .unwrap(); + + assert!(output.status.success()); + Some(output) + } + } + + /// This test ensures that `run_node_until_exit` aborts waiting for "stuck" tasks after 60 + /// seconds, aka doesn't wait until they are finished (which may never happen). + #[test] + fn ensure_run_until_exit_is_not_blocking_indefinitely() { + let output = run_test_in_another_process( + "ensure_run_until_exit_is_not_blocking_indefinitely", + || { + sp_tracing::try_init_simple(); + + let runner = create_runner(); + + runner + .run_node_until_exit(move |cfg| async move { + let task_manager = + TaskManager::new(cfg.tokio_handle.clone(), None).unwrap(); + let (sender, receiver) = futures::channel::oneshot::channel(); + + // We need to use `spawn_blocking` here so that we get a dedicated thread + // for our future. This future is more blocking code that will never end. + task_manager + .spawn_handle() + .spawn_blocking("test", None, async move { + let _ = sender.send(()); + loop { + std::thread::sleep(Duration::from_secs(30)); + } + }); + + task_manager.spawn_essential_handle().spawn_blocking( + "test2", + None, + async { + // Let's stop this essential task directly when our other task + // started. It will signal that the task manager should end. + let _ = receiver.await; + }, + ); + + Ok::<_, sc_service::Error>(task_manager) + }) + .unwrap_err(); + }, + ); + + let Some(output) = output else { return }; + + let stderr = dbg!(String::from_utf8(output.stderr).unwrap()); + + assert!( + stderr.contains("Task \"test\" was still running after waiting 60 seconds to finish.") + ); + assert!(!stderr + .contains("Task \"test2\" was still running after waiting 60 seconds to finish.")); + } +} diff --git a/substrate/cli/src/signals.rs b/substrate/cli/src/signals.rs new file mode 100644 index 00000000000..b4c6648b5ff --- /dev/null +++ b/substrate/cli/src/signals.rs @@ -0,0 +1,77 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use futures::{ + future::{self, BoxFuture, FutureExt}, + pin_mut, select, Future, +}; + +use sc_service::Error as ServiceError; + +/// Abstraction over OS signals to handle the shutdown of the node smoothly. +/// +/// On `unix` this represents `SigInt` and `SigTerm`. +pub struct Signals(BoxFuture<'static, ()>); + +impl Signals { + /// Return the signals future. + pub fn future(self) -> BoxFuture<'static, ()> { + self.0 + } + + /// Capture the relevant signals to handle shutdown of the node smoothly. + /// + /// Needs to be called in a Tokio context to have access to the tokio reactor. + #[cfg(target_family = "unix")] + pub fn capture() -> std::result::Result { + use tokio::signal::unix::{signal, SignalKind}; + + let mut stream_int = signal(SignalKind::interrupt()).map_err(ServiceError::Io)?; + let mut stream_term = signal(SignalKind::terminate()).map_err(ServiceError::Io)?; + + Ok(Signals( + async move { + future::select(stream_int.recv().boxed(), stream_term.recv().boxed()).await; + } + .boxed(), + )) + } + + /// Capture the relevant signals to handle shutdown of the node smoothly. + /// + /// Needs to be called in a Tokio context to have access to the tokio reactor. + #[cfg(not(unix))] + pub fn capture() -> Result { + use tokio::signal::ctrl_c; + + Ok(Signals( + async move { + let _ = ctrl_c().await; + } + .boxed(), + )) + } + + /// A dummy signal that never returns. + pub fn dummy() -> Self { + Self(future::pending().boxed()) + } + + /// Run a future task until receive a signal. + pub async fn run_until_signal(self, func: F) -> Result<(), E> + where + F: Future> + future::FusedFuture, + E: std::error::Error + Send + Sync + 'static, + { + let signals = self.future().fuse(); + + pin_mut!(func, signals); + + select! { + _ = signals => {}, + res = func => res?, + } + + Ok(()) + } +} diff --git a/substrate/rpc-servers/Cargo.toml b/substrate/rpc-servers/Cargo.toml new file mode 100644 index 00000000000..f4887700aa2 --- /dev/null +++ b/substrate/rpc-servers/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "sc-rpc-server" +version = "17.1.2" +authors.workspace = true +edition = "2021" +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" +homepage.workspace = true +repository.workspace = true +description = "Substrate RPC servers." +readme = "README.md" + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +dyn-clone = { workspace = true } +forwarded-header-value = { workspace = true } +futures = { workspace = true, default-features = true } +governor = { workspace = true } +http = { workspace = true } +http-body-util = { workspace = true } +hyper = { workspace = true } +ip_network = { workspace = true } +jsonrpsee = { features = ["server"], workspace = true } +log = { workspace = true, default-features = true } +prometheus-endpoint.workspace = true +prometheus-endpoint.default-features = true +sc-rpc-api.workspace = true +serde = { workspace = true } +serde_json = { workspace = true, default-features = true } +tokio = { features = ["parking_lot"], workspace = true, default-features = true } +tower = { workspace = true, features = ["util"] } +tower-http = { workspace = true, features = ["cors"] } +gear-workspace-hack.workspace = true diff --git a/substrate/rpc-servers/README.md b/substrate/rpc-servers/README.md new file mode 100644 index 00000000000..4ca101fcd05 --- /dev/null +++ b/substrate/rpc-servers/README.md @@ -0,0 +1,8 @@ +Substrate RPC servers. + +License: GPL-3.0-or-later WITH Classpath-exception-2.0 + + +## Release + +Polkadot SDK stable2409 diff --git a/substrate/rpc-servers/src/lib.rs b/substrate/rpc-servers/src/lib.rs new file mode 100644 index 00000000000..46a4a1d78fc --- /dev/null +++ b/substrate/rpc-servers/src/lib.rs @@ -0,0 +1,285 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Substrate RPC servers. + +#![warn(missing_docs)] + +pub mod middleware; +pub mod utils; + +use std::{error::Error as StdError, sync::Arc, time::Duration}; + +use jsonrpsee::{ + server::{serve_with_graceful_shutdown, stop_channel, ws, PingConfig, StopHandle}, + Methods, RpcModule, +}; +use middleware::{MethodLimiters, NodeHealthProxyLayer}; +use tower::Service; +use utils::{ + build_rpc_api, deny_unsafe, format_listen_addrs, get_proxy_ip, ListenAddrError, RpcSettings, +}; + +pub use ip_network::IpNetwork; +pub use jsonrpsee::{ + core::id_providers::{RandomIntegerIdProvider, RandomStringIdProvider}, + server::{middleware::rpc::RpcServiceBuilder, BatchRequestConfig}, +}; +pub use middleware::{Metrics, MiddlewareLayer, RpcMethodLimit, RpcMetrics}; +pub use utils::{RpcEndpoint, RpcMethods}; + +const MEGABYTE: u32 = 1024 * 1024; + +/// Type alias for the JSON-RPC server. +pub type Server = jsonrpsee::server::ServerHandle; + +/// Trait for providing subscription IDs that can be cloned. +pub trait SubscriptionIdProvider: + jsonrpsee::core::traits::IdProvider + dyn_clone::DynClone +{ +} + +dyn_clone::clone_trait_object!(SubscriptionIdProvider); + +/// RPC server configuration. +#[derive(Debug)] +pub struct Config { + /// RPC interfaces to start. + pub endpoints: Vec, + /// Metrics. + pub metrics: Option, + /// RPC API. + pub rpc_api: RpcModule, + /// Subscription ID provider. + pub id_provider: Option>, + /// Node-wide per-method rate and concurrency budgets. + pub method_limits: Vec, + /// Tokio runtime handle. + pub tokio_handle: tokio::runtime::Handle, +} + +#[derive(Debug, Clone)] +struct PerConnection { + methods: Methods, + stop_handle: StopHandle, + metrics: Option, + method_limiters: Arc, + tokio_handle: tokio::runtime::Handle, +} + +/// Start RPC server listening on given address. +pub async fn start_server(config: Config) -> Result> +where + M: Send + Sync, +{ + let Config { + endpoints, + metrics, + tokio_handle, + rpc_api, + id_provider, + method_limits, + } = config; + + let method_limiters = Arc::new( + MethodLimiters::try_new(method_limits) + .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidInput, error))?, + ); + + let (stop_handle, server_handle) = stop_channel(); + let cfg = PerConnection { + methods: build_rpc_api(rpc_api).into(), + metrics, + method_limiters, + tokio_handle: tokio_handle.clone(), + stop_handle, + }; + + let mut local_addrs = Vec::new(); + + for endpoint in endpoints { + let allowed_to_fail = endpoint.is_optional; + let local_addr = endpoint.listen_addr; + + let mut listener = match endpoint.bind().await { + Ok(l) => l, + Err(e) if allowed_to_fail => { + log::debug!(target: "rpc", "JSON-RPC server failed to bind optional address: {:?}, error: {:?}", local_addr, e); + continue; + } + Err(e) => return Err(e), + }; + let local_addr = listener.local_addr(); + local_addrs.push(local_addr); + let cfg = cfg.clone(); + + let RpcSettings { + batch_config, + max_connections, + max_payload_in_mb, + max_payload_out_mb, + max_buffer_capacity_per_connection, + max_subscriptions_per_connection, + rpc_methods, + rate_limit_trust_proxy_headers, + rate_limit_whitelisted_ips, + host_filter, + cors, + rate_limit, + } = listener.rpc_settings(); + + let http_middleware = tower::ServiceBuilder::new() + .option_layer(host_filter) + // Proxy `GET /health, /health/readiness` requests to the internal + // `system_health` method. + .layer(NodeHealthProxyLayer) + .layer(cors); + + let mut builder = jsonrpsee::server::Server::builder() + .max_request_body_size(max_payload_in_mb.saturating_mul(MEGABYTE)) + .max_response_body_size(max_payload_out_mb.saturating_mul(MEGABYTE)) + .max_connections(max_connections) + .max_subscriptions_per_connection(max_subscriptions_per_connection) + .enable_ws_ping( + PingConfig::new() + .ping_interval(Duration::from_secs(30)) + .inactive_limit(Duration::from_secs(60)) + .max_failures(3), + ) + .set_http_middleware(http_middleware) + .set_message_buffer_capacity(max_buffer_capacity_per_connection) + .set_batch_request_config(batch_config) + .custom_tokio_runtime(cfg.tokio_handle.clone()); + + if let Some(provider) = id_provider.clone() { + builder = builder.set_id_provider(provider); + } else { + builder = builder.set_id_provider(RandomStringIdProvider::new(16)); + }; + + let service_builder = builder.to_service_builder(); + let deny_unsafe = deny_unsafe(&local_addr, &rpc_methods); + + tokio_handle.spawn(async move { + loop { + let (sock, remote_addr) = tokio::select! { + res = listener.accept() => { + match res { + Ok(s) => s, + Err(e) => { + log::debug!(target: "rpc", "Failed to accept connection: {:?}", e); + continue; + } + } + } + _ = cfg.stop_handle.clone().shutdown() => break, + }; + + let ip = remote_addr.ip(); + let cfg2 = cfg.clone(); + let service_builder2 = service_builder.clone(); + let rate_limit_whitelisted_ips2 = rate_limit_whitelisted_ips.clone(); + + let svc = + tower::service_fn(move |mut req: http::Request| { + req.extensions_mut().insert(deny_unsafe); + + let PerConnection { + methods, + metrics, + method_limiters, + tokio_handle, + stop_handle, + } = cfg2.clone(); + let service_builder = service_builder2.clone(); + + let proxy_ip = + if rate_limit_trust_proxy_headers { get_proxy_ip(&req) } else { None }; + let trusted_client_ip = proxy_ip.unwrap_or(ip); + + let rate_limit_cfg = if rate_limit_whitelisted_ips2 + .iter() + .any(|ips| ips.contains(proxy_ip.unwrap_or(ip))) + { + log::debug!(target: "rpc", "ip={ip}, proxy_ip={:?} is trusted, disabling rate-limit", proxy_ip); + None + } else { + if !rate_limit_whitelisted_ips2.is_empty() { + log::debug!(target: "rpc", "ip={ip}, proxy_ip={:?} is not trusted, rate-limit enabled", proxy_ip); + } + rate_limit + }; + + let is_websocket = ws::is_upgrade_request(&req); + let transport_label = if is_websocket { "ws" } else { "http" }; + + let mut middleware_layer = MiddlewareLayer::new().with_method_limiters( + method_limiters, + transport_label, + trusted_client_ip, + ); + if let Some(metrics) = metrics { + middleware_layer = + middleware_layer.with_metrics(Metrics::new(metrics, transport_label)); + } + if let Some(rate_limit) = rate_limit_cfg { + middleware_layer = + middleware_layer.with_rate_limit_per_minute(rate_limit); + } + let middleware_layer = Some(middleware_layer); + + let rpc_middleware = + RpcServiceBuilder::new().option_layer(middleware_layer.clone()); + let mut svc = service_builder + .set_rpc_middleware(rpc_middleware) + .build(methods, stop_handle); + + async move { + if is_websocket { + let on_disconnect = svc.on_session_closed(); + + // Spawn a task to handle when the connection is closed. + tokio_handle.spawn(async move { + let now = std::time::Instant::now(); + if let Some(middleware) = middleware_layer.as_ref() { + middleware.ws_connect(); + } + on_disconnect.await; + if let Some(middleware) = middleware_layer.as_ref() { + middleware.ws_disconnect(now); + } + }); + } + + svc.call(req).await + } + }); + + cfg.tokio_handle.spawn(serve_with_graceful_shutdown( + sock, + svc, + cfg.stop_handle.clone().shutdown(), + )); + } + }); + } + + if local_addrs.is_empty() { + return Err(Box::new(ListenAddrError)); + } + + // The previous logging format was before + // `Running JSON-RPC server: addr=127.0.0.1:9944, allowed origins=["*"]` + // + // The new format is `Running JSON-RPC server: addr=` + // with the exception that for a single address it will be `Running JSON-RPC server: addr=addr,` + // with a trailing comma. + // + // This is to make it work with old scripts/utils that parse the logs. + log::info!( + "Running JSON-RPC server: addr={}", + format_listen_addrs(&local_addrs) + ); + + Ok(server_handle) +} diff --git a/substrate/rpc-servers/src/middleware/method_limit.rs b/substrate/rpc-servers/src/middleware/method_limit.rs new file mode 100644 index 00000000000..d79e4b809ed --- /dev/null +++ b/substrate/rpc-servers/src/middleware/method_limit.rs @@ -0,0 +1,329 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Node-wide per-method RPC rate and concurrency budgets. + +use std::{ + collections::{HashMap, HashSet}, + num::{NonZeroU32, NonZeroUsize}, + str::FromStr, + sync::Arc, +}; + +use tokio::sync::{OwnedSemaphorePermit, Semaphore}; + +use super::RateLimit; + +/// A node-wide rate and concurrency limit for one or more RPC methods. +/// +/// The first method is the canonical method used for metrics and logging. Every subsequent +/// method is an alias sharing the same budget. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RpcMethodLimit { + methods: Vec, + calls_per_minute: NonZeroU32, + max_in_flight: NonZeroUsize, +} + +impl RpcMethodLimit { + /// Validates that every method belongs to at most one budget group. + pub fn validate_all(limits: &[Self]) -> Result<(), String> { + let mut seen = HashSet::new(); + for limit in limits { + for method in &limit.methods { + if !seen.insert(method) { + return Err(format!( + "RPC method `{method}` appears in more than one method-limit group" + )); + } + } + } + Ok(()) + } +} + +impl FromStr for RpcMethodLimit { + type Err = String; + + fn from_str(value: &str) -> Result { + let (methods, limits) = value.split_once('=').ok_or_else(|| { + "RPC method limit must use METHOD[,ALIAS...]=CALLS_PER_MINUTE,MAX_IN_FLIGHT".to_owned() + })?; + + let methods = methods.split(',').map(str::to_owned).collect::>(); + if methods.is_empty() || methods.iter().any(String::is_empty) { + return Err("RPC method limit contains an empty method name or alias".to_owned()); + } + + let values = limits.split(',').collect::>(); + if values.len() != 2 || values.iter().any(|value| value.is_empty()) { + return Err( + "RPC method limit must contain exactly two non-zero numbers after `=`".to_owned(), + ); + } + + let calls_per_minute = values[0].parse::().map_err(|_| { + "RPC method limit calls-per-minute must be a non-zero 32-bit integer".to_owned() + })?; + let max_in_flight = values[1].parse::().map_err(|_| { + "RPC method limit max-in-flight must be a non-zero usize integer".to_owned() + })?; + if max_in_flight.get() > Semaphore::MAX_PERMITS { + return Err(format!( + "RPC method limit max-in-flight must not exceed {}", + Semaphore::MAX_PERMITS + )); + } + + Ok(Self { + methods, + calls_per_minute, + max_in_flight, + }) + } +} + +#[derive(Debug)] +struct MethodLimiter { + canonical: Arc, + max_in_flight: usize, + semaphore: Arc, + rate_limit: RateLimit, +} + +/// Shared node-wide method limiter registry. +#[derive(Debug)] +pub(crate) struct MethodLimiters { + methods: HashMap>, +} + +impl MethodLimiters { + /// Builds the registry and validates duplicate method names. + pub(crate) fn try_new(limits: Vec) -> Result { + RpcMethodLimit::validate_all(&limits)?; + + let mut methods = HashMap::new(); + for limit in limits { + log::info!( + target: "rpc", + "Configured RPC method limit: methods={:?}, calls_per_minute={}, max_in_flight={}", + limit.methods, + limit.calls_per_minute, + limit.max_in_flight, + ); + + let canonical: Arc = Arc::from(limit.methods[0].as_str()); + let max_in_flight = limit.max_in_flight.get(); + let limiter = Arc::new(MethodLimiter { + canonical: canonical.clone(), + max_in_flight, + semaphore: Arc::new(Semaphore::new(max_in_flight)), + rate_limit: RateLimit::per_minute(limit.calls_per_minute), + }); + + for method in limit.methods { + methods.insert(method, limiter.clone()); + } + } + + Ok(Self { methods }) + } + + /// Returns the canonical method for a configured literal method name. + pub(crate) fn canonical(&self, method: &str) -> Option> { + self.methods + .get(method) + .map(|limiter| limiter.canonical.clone()) + } + + /// Returns the current in-flight count and maximum for a configured method. + /// + /// Literal aliases resolve to the same limiter and therefore share this + /// snapshot. Unconfigured methods return `None`. + pub(crate) fn concurrency(&self, method: &str) -> Option<(usize, usize)> { + let limiter = self.methods.get(method)?; + let max_in_flight = limiter.max_in_flight; + let in_flight = max_in_flight.saturating_sub(limiter.semaphore.available_permits()); + Some((in_flight, max_in_flight)) + } + + /// Tries to admit one call without waiting. + /// + /// An unconfigured method returns `Ok(None)`. A configured method returns an + /// owned semaphore permit on success; the permit is held by the admission + /// until the request future completes or is dropped. + pub(crate) fn try_acquire( + &self, + method: &str, + ) -> Result, MethodLimitReason> { + let Some(limiter) = self.methods.get(method) else { + return Ok(None); + }; + + let permit = limiter + .semaphore + .clone() + .try_acquire_owned() + .map_err(|_| MethodLimitReason::Concurrency)?; + + if limiter.rate_limit.inner.check().is_err() { + // Dropping the owned permit immediately releases the concurrency slot + // before returning the rejection. + drop(permit); + return Err(MethodLimitReason::Rate); + } + + Ok(Some(MethodAdmission { + canonical: limiter.canonical.clone(), + _permit: permit, + })) + } +} + +/// The reason a configured method call was rejected. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum MethodLimitReason { + /// The method's calls-per-minute bucket is exhausted. + Rate, + /// The method's in-flight semaphore is exhausted. + Concurrency, +} + +impl MethodLimitReason { + /// Returns the bounded metric/log label for this reason. + pub(crate) const fn as_str(self) -> &'static str { + match self { + Self::Rate => "rate", + Self::Concurrency => "concurrency", + } + } +} + +/// An admitted method call and its owned in-flight permit. +#[derive(Debug)] +pub(crate) struct MethodAdmission { + pub(crate) canonical: Arc, + pub(crate) _permit: OwnedSemaphorePermit, +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse(value: &str) -> RpcMethodLimit { + value.parse().expect("valid RPC method limit") + } + + #[test] + fn parses_single_method_and_aliases() { + let limit = parse("state_getRuntimeVersion,chain_getRuntimeVersion=60,2"); + assert_eq!(limit.methods.len(), 2); + assert_eq!(limit.methods[0], "state_getRuntimeVersion"); + assert_eq!(limit.methods[1], "chain_getRuntimeVersion"); + assert_eq!(limit.calls_per_minute.get(), 60); + assert_eq!(limit.max_in_flight.get(), 2); + } + + #[test] + fn validates_repeated_groups_and_rejects_invalid_values() { + let repeated = vec![ + parse("state_getMetadata=30,1"), + parse("state_getMetadata=10,2"), + ]; + assert!(RpcMethodLimit::validate_all(&repeated).is_err()); + + for value in [ + "state_getMetadata=0,1", + "state_getMetadata=1,0", + "state_getMetadata=1", + "state_getMetadata=1,2,3", + "state_getMetadata,,state_getRuntimeVersion=1,2", + "=1,2", + "state_getMetadata=,2", + "state_getMetadata=1,", + "state_getMetadata", + ] { + assert!( + value.parse::().is_err(), + "{value} should fail" + ); + } + + let too_many_permits = format!( + "state_getMetadata=1,{}", + Semaphore::MAX_PERMITS.saturating_add(1) + ); + assert!(too_many_permits.parse::().is_err()); + } + + #[test] + fn aliases_share_permit_and_methods_are_independent() { + let limits = vec![ + parse("state_getRuntimeVersion,chain_getRuntimeVersion=60,1"), + parse("state_getMetadata=60,1"), + ]; + let limiters = MethodLimiters::try_new(limits).unwrap(); + + let first = limiters + .try_acquire("state_getRuntimeVersion") + .unwrap() + .unwrap(); + assert_eq!(&*first.canonical, "state_getRuntimeVersion"); + assert!(matches!( + limiters.try_acquire("chain_getRuntimeVersion"), + Err(MethodLimitReason::Concurrency) + )); + assert!(limiters.try_acquire("state_getMetadata").unwrap().is_some()); + drop(first); + assert!(limiters + .try_acquire("chain_getRuntimeVersion") + .unwrap() + .is_some()); + assert!(limiters.try_acquire("system_health").unwrap().is_none()); + } + + #[test] + fn concurrency_snapshot_tracks_aliases_and_permit_lifetime() { + let limiters = MethodLimiters::try_new(vec![parse( + "state_getRuntimeVersion,chain_getRuntimeVersion=60,2", + )]) + .unwrap(); + + assert_eq!(limiters.concurrency("system_health"), None); + assert_eq!( + limiters.concurrency("state_getRuntimeVersion"), + Some((0, 2)) + ); + + let admission = limiters + .try_acquire("state_getRuntimeVersion") + .unwrap() + .unwrap(); + assert_eq!( + limiters.concurrency("chain_getRuntimeVersion"), + Some((1, 2)) + ); + + drop(admission); + assert_eq!( + limiters.concurrency("state_getRuntimeVersion"), + Some((0, 2)) + ); + } + + #[test] + fn rate_rejection_is_immediate_and_releases_permit() { + let limiters = MethodLimiters::try_new(vec![parse("state_getMetadata=1,1")]).unwrap(); + let first = limiters.try_acquire("state_getMetadata").unwrap().unwrap(); + assert!(matches!( + limiters.try_acquire("state_getMetadata"), + Err(MethodLimitReason::Concurrency) + )); + drop(first); + assert!(matches!( + limiters.try_acquire("state_getMetadata"), + Err(MethodLimitReason::Rate) + )); + assert!(limiters.try_acquire("state_getMetadata").is_err()); + } +} diff --git a/substrate/rpc-servers/src/middleware/metrics.rs b/substrate/rpc-servers/src/middleware/metrics.rs new file mode 100644 index 00000000000..bd0de6c4fa4 --- /dev/null +++ b/substrate/rpc-servers/src/middleware/metrics.rs @@ -0,0 +1,407 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! RPC middleware to collect prometheus metrics on RPC calls. + +use std::time::Instant; + +use jsonrpsee::{types::Request, MethodResponse}; +use prometheus_endpoint::{ + register, Counter, CounterVec, Gauge, GaugeVec, HistogramOpts, HistogramVec, Opts, + PrometheusError, Registry, U64, +}; + +/// Histogram time buckets in microseconds. +const HISTOGRAM_BUCKETS: [f64; 11] = [ + 5.0, + 25.0, + 100.0, + 500.0, + 1_000.0, + 2_500.0, + 10_000.0, + 25_000.0, + 100_000.0, + 1_000_000.0, + 10_000_000.0, +]; + +/// Metrics for RPC middleware storing information about the number of requests started/completed, +/// calls started/completed and their timings. +#[derive(Debug, Clone)] +pub struct RpcMetrics { + /// Histogram over RPC execution times. + calls_time: HistogramVec, + /// Number of calls started. + calls_started: CounterVec, + /// Number of calls completed. + calls_finished: CounterVec, + /// Number of calls admitted by a node-wide method limit. + method_limit_admitted: CounterVec, + /// Number of calls rejected by a node-wide method limit. + method_limit_rejected: CounterVec, + /// Number of calls currently in flight under a node-wide method limit. + method_limit_in_flight: GaugeVec, + /// Number of Websocket sessions opened. + ws_sessions_opened: Option>, + /// Number of Websocket sessions closed. + ws_sessions_closed: Option>, + /// Histogram over RPC websocket sessions. + ws_sessions_time: HistogramVec, +} + +impl RpcMetrics { + /// Create an instance of metrics + pub fn new(metrics_registry: Option<&Registry>) -> Result, PrometheusError> { + if let Some(metrics_registry) = metrics_registry { + Ok(Some(Self { + calls_time: register( + HistogramVec::new( + HistogramOpts::new( + "substrate_rpc_calls_time", + "Total time [μs] of processed RPC calls", + ) + .buckets(HISTOGRAM_BUCKETS.to_vec()), + &["protocol", "method", "is_rate_limited"], + )?, + metrics_registry, + )?, + calls_started: register( + CounterVec::new( + Opts::new( + "substrate_rpc_calls_started", + "Number of received RPC calls (unique un-batched requests)", + ), + &["protocol", "method"], + )?, + metrics_registry, + )?, + calls_finished: register( + CounterVec::new( + Opts::new( + "substrate_rpc_calls_finished", + "Number of processed RPC calls (unique un-batched requests)", + ), + &["protocol", "method", "is_error", "is_rate_limited"], + )?, + metrics_registry, + )?, + method_limit_admitted: register( + CounterVec::new( + Opts::new( + "substrate_rpc_method_limit_admitted", + "Number of RPC calls admitted by a node-wide method limit", + ), + &["protocol", "method"], + )?, + metrics_registry, + )?, + method_limit_rejected: register( + CounterVec::new( + Opts::new( + "substrate_rpc_method_limit_rejected", + "Number of RPC calls rejected by a node-wide method limit", + ), + &["protocol", "method", "reason"], + )?, + metrics_registry, + )?, + method_limit_in_flight: register( + GaugeVec::new( + Opts::new( + "substrate_rpc_method_limit_in_flight", + "Number of RPC calls currently in flight under a node-wide method limit", + ), + &["protocol", "method"], + )?, + metrics_registry, + )?, + ws_sessions_opened: register( + Counter::new( + "substrate_rpc_sessions_opened", + "Number of persistent RPC sessions opened", + )?, + metrics_registry, + )? + .into(), + ws_sessions_closed: register( + Counter::new( + "substrate_rpc_sessions_closed", + "Number of persistent RPC sessions closed", + )?, + metrics_registry, + )? + .into(), + ws_sessions_time: register( + HistogramVec::new( + HistogramOpts::new( + "substrate_rpc_sessions_time", + "Total time [s] for each websocket session", + ) + .buckets(HISTOGRAM_BUCKETS.to_vec()), + &["protocol"], + )?, + metrics_registry, + )?, + })) + } else { + Ok(None) + } + } + + pub(crate) fn ws_connect(&self) { + if let Some(counter) = self.ws_sessions_opened.as_ref() { + counter.inc(); + } + } + + pub(crate) fn ws_disconnect(&self, now: Instant) { + let micros = now.elapsed().as_secs(); + + if let Some(counter) = self.ws_sessions_closed.as_ref() { + counter.inc(); + } + self.ws_sessions_time + .with_label_values(&["ws"]) + .observe(micros as _); + } + + pub(crate) fn on_call(&self, req: &Request, transport_label: &'static str) { + log::trace!( + target: "rpc_metrics", + "[{transport_label}] on_call name={}", + req.method_name(), + ); + + self.calls_started + .with_label_values(&[transport_label, req.method_name()]) + .inc(); + } + + pub(crate) fn on_response( + &self, + req: &Request, + rp: &MethodResponse, + is_rate_limited: bool, + transport_label: &'static str, + now: Instant, + ) { + log::trace!(target: "rpc_metrics", "[{transport_label}] on_response started_at={:?}", now); + + let micros = now.elapsed().as_micros(); + log::debug!( + target: "rpc_metrics", + "[{transport_label}] {} call took {} μs", + req.method_name(), + micros, + ); + self.calls_time + .with_label_values(&[ + transport_label, + req.method_name(), + if is_rate_limited { "true" } else { "false" }, + ]) + .observe(micros as _); + self.calls_finished + .with_label_values(&[ + transport_label, + req.method_name(), + // the label "is_error", so `success` should be regarded as false + // and vice-versa to be registered correctly. + if rp.is_success() { "false" } else { "true" }, + if is_rate_limited { "true" } else { "false" }, + ]) + .inc(); + } + + pub(crate) fn method_limit_admitted(&self, method: &str, transport_label: &'static str) { + self.method_limit_admitted + .with_label_values(&[transport_label, method]) + .inc(); + } + + pub(crate) fn method_limit_rejected( + &self, + method: &str, + reason: &str, + transport_label: &'static str, + ) { + self.method_limit_rejected + .with_label_values(&[transport_label, method, reason]) + .inc(); + } + + pub(crate) fn method_limit_in_flight( + &self, + method: &str, + transport_label: &'static str, + ) -> MethodLimitInFlightGuard { + let gauge = self + .method_limit_in_flight + .with_label_values(&[transport_label, method]); + gauge.inc(); + MethodLimitInFlightGuard { gauge } + } +} + +/// RAII guard for a method-limit in-flight metric. +pub(crate) struct MethodLimitInFlightGuard { + gauge: Gauge, +} + +impl Drop for MethodLimitInFlightGuard { + fn drop(&mut self) { + self.gauge.dec(); + } +} + +/// Metrics with transport label. +#[derive(Clone, Debug)] +pub struct Metrics { + pub(crate) inner: RpcMetrics, + pub(crate) transport_label: &'static str, +} + +impl Metrics { + /// Create a new [`Metrics`]. + pub fn new(metrics: RpcMetrics, transport_label: &'static str) -> Self { + Self { + inner: metrics, + transport_label, + } + } + + pub(crate) fn ws_connect(&self) { + self.inner.ws_connect(); + } + + pub(crate) fn ws_disconnect(&self, now: Instant) { + self.inner.ws_disconnect(now) + } + + pub(crate) fn on_call(&self, req: &Request) { + self.inner.on_call(req, self.transport_label) + } + + pub(crate) fn on_response( + &self, + req: &Request, + rp: &MethodResponse, + is_rate_limited: bool, + now: Instant, + ) { + self.inner + .on_response(req, rp, is_rate_limited, self.transport_label, now) + } + pub(crate) fn method_limit_admitted(&self, method: &str) { + self.inner + .method_limit_admitted(method, self.transport_label) + } + + pub(crate) fn method_limit_rejected(&self, method: &str, reason: &str) { + self.inner + .method_limit_rejected(method, reason, self.transport_label) + } + + pub(crate) fn method_limit_in_flight(&self, method: &str) -> MethodLimitInFlightGuard { + self.inner + .method_limit_in_flight(method, self.transport_label) + } +} + +#[cfg(test)] +mod method_limit_metrics_tests { + use std::{borrow::Cow, net::IpAddr, sync::Arc, task::Poll}; + + use futures::{future::BoxFuture, FutureExt}; + use jsonrpsee::{ + server::middleware::rpc::RpcServiceT, + types::{ErrorObject, Id, Request}, + MethodResponse, + }; + use prometheus_endpoint::Registry; + use tower::Layer; + + use crate::middleware::{MethodLimiters, MiddlewareLayer}; + + use super::*; + + #[derive(Clone)] + struct PendingService; + + impl<'a> RpcServiceT<'a> for PendingService { + type Future = BoxFuture<'a, MethodResponse>; + + fn call(&self, req: Request<'a>) -> Self::Future { + let id = req.id; + async move { + futures::future::pending::<()>().await; + MethodResponse::error(id, ErrorObject::owned(-1, "unreachable", None::<()>)) + } + .boxed() + } + } + + fn request(method: &'static str, id: u64) -> Request<'static> { + Request::new(Cow::Borrowed(method), None, Id::Number(id)) + } + + #[tokio::test] + async fn method_limit_metrics_follow_middleware_dispatch() { + let registry = Registry::new(); + let rpc = RpcMetrics::new(Some(®istry)) + .expect("metrics registration succeeds") + .expect("metrics are enabled"); + let limiters = Arc::new( + MethodLimiters::try_new(vec!["state_getRuntimeVersion,chain_getRuntimeVersion=1,1" + .parse() + .expect("valid method limit")]) + .expect("method limit registration succeeds"), + ); + let service = MiddlewareLayer::new() + .with_method_limiters(limiters, "http", IpAddr::from([127, 0, 0, 1])) + .with_metrics(Metrics::new(rpc.clone(), "http")) + .layer(PendingService); + + let mut first = service.call(request("state_getRuntimeVersion", 1)); + assert!(matches!(futures::poll!(first.as_mut()), Poll::Pending)); + assert_eq!( + rpc.method_limit_admitted + .with_label_values(&["http", "state_getRuntimeVersion"]) + .get(), + 1 + ); + assert_eq!( + rpc.method_limit_in_flight + .with_label_values(&["http", "state_getRuntimeVersion"]) + .get(), + 1 + ); + + let concurrency = service.call(request("chain_getRuntimeVersion", 2)).await; + assert_eq!(concurrency.as_error_code(), Some(-32999)); + assert_eq!( + rpc.method_limit_rejected + .with_label_values(&["http", "state_getRuntimeVersion", "concurrency"]) + .get(), + 1 + ); + + drop(first); + assert_eq!( + rpc.method_limit_in_flight + .with_label_values(&["http", "state_getRuntimeVersion"]) + .get(), + 0 + ); + + let rate = service.call(request("chain_getRuntimeVersion", 3)).await; + assert_eq!(rate.as_error_code(), Some(-32999)); + assert_eq!( + rpc.method_limit_rejected + .with_label_values(&["http", "state_getRuntimeVersion", "rate"]) + .get(), + 1 + ); + } +} diff --git a/substrate/rpc-servers/src/middleware/mod.rs b/substrate/rpc-servers/src/middleware/mod.rs new file mode 100644 index 00000000000..583009fcb79 --- /dev/null +++ b/substrate/rpc-servers/src/middleware/mod.rs @@ -0,0 +1,548 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! JSON-RPC specific middleware. + +use std::{ + net::IpAddr, + num::NonZeroU32, + sync::Arc, + time::{Duration, Instant}, +}; + +use futures::future::{BoxFuture, FutureExt}; +use governor::{clock::Clock, Jitter}; +use jsonrpsee::{ + server::middleware::rpc::RpcServiceT, + types::{ErrorObject, Id, Request}, + MethodResponse, +}; + +mod method_limit; +mod metrics; +mod node_health; +mod rate_limit; + +pub(crate) use method_limit::MethodLimiters; +pub use method_limit::RpcMethodLimit; +pub use metrics::*; +pub use node_health::*; +pub use rate_limit::*; + +const MAX_JITTER: Duration = Duration::from_millis(50); +const MAX_RETRIES: usize = 10; + +/// JSON-RPC middleware layer. +#[derive(Debug, Clone, Default)] +pub struct MiddlewareLayer { + rate_limit: Option, + metrics: Option, + method_limiters: Option>, + protocol: &'static str, + trusted_client_ip: Option, +} + +impl MiddlewareLayer { + /// Create an empty MiddlewareLayer. + pub fn new() -> Self { + Self::default() + } + + /// Configure node-wide method limits and call attribution. + pub(crate) fn with_method_limiters( + self, + method_limiters: Arc, + protocol: &'static str, + trusted_client_ip: IpAddr, + ) -> Self { + Self { + rate_limit: self.rate_limit, + metrics: self.metrics, + method_limiters: Some(method_limiters), + protocol, + trusted_client_ip: Some(trusted_client_ip), + } + } + + /// Enable new rate limit middleware enforced per minute. + pub fn with_rate_limit_per_minute(self, n: NonZeroU32) -> Self { + Self { + rate_limit: Some(RateLimit::per_minute(n)), + ..self + } + } + + /// Enable metrics middleware. + pub fn with_metrics(self, metrics: Metrics) -> Self { + Self { + metrics: Some(metrics), + ..self + } + } + + /// Register a new websocket connection. + pub fn ws_connect(&self) { + if let Some(metrics) = self.metrics.as_ref() { + metrics.ws_connect(); + } + } + + /// Register that a websocket connection was closed. + pub fn ws_disconnect(&self, now: Instant) { + if let Some(metrics) = self.metrics.as_ref() { + metrics.ws_disconnect(now); + } + } +} + +impl tower::Layer for MiddlewareLayer { + type Service = Middleware; + + fn layer(&self, service: S) -> Self::Service { + Middleware { + service, + rate_limit: self.rate_limit.clone(), + metrics: self.metrics.clone(), + method_limiters: self.method_limiters.clone(), + protocol: self.protocol, + trusted_client_ip: self.trusted_client_ip, + } + } +} + +/// JSON-RPC middleware that handles metrics +/// and rate-limiting. +/// +/// These are part of the same middleware +/// because the metrics needs to know whether +/// a call was rate-limited or not because +/// it will impact the roundtrip for a call. +pub struct Middleware { + service: S, + rate_limit: Option, + metrics: Option, + method_limiters: Option>, + protocol: &'static str, + trusted_client_ip: Option, +} + +impl<'a, S> RpcServiceT<'a> for Middleware +where + S: Send + Sync + RpcServiceT<'a> + Clone + 'static, +{ + type Future = BoxFuture<'a, MethodResponse>; + + fn call(&self, req: Request<'a>) -> Self::Future { + let now = Instant::now(); + + if let Some(metrics) = self.metrics.as_ref() { + metrics.on_call(&req); + } + + let service = self.service.clone(); + let rate_limit = self.rate_limit.clone(); + let metrics = self.metrics.clone(); + let method_limiters = self.method_limiters.clone(); + let protocol = self.protocol; + let trusted_client_ip = self.trusted_client_ip; + + async move { + let method_name = req.method_name(); + let method_admission = if let Some(limiters) = method_limiters.as_ref() { + match limiters.try_acquire(method_name) { + Ok(admission) => admission, + Err(reason) => { + let canonical_name = limiters.canonical(method_name); + let canonical = canonical_name.as_deref().unwrap_or(method_name); + let rp = reject_method_limit(req.id.clone()); + if let Some(metrics) = metrics.as_ref() { + metrics.method_limit_rejected(canonical, reason.as_str()); + metrics.on_response(&req, &rp, true, now); + } + log_rpc_call( + trusted_client_ip, + protocol, + method_name, + canonical, + Some(limiters.as_ref()), + now, + "limited", + reason.as_str(), + ); + return rp; + } + } + } else { + None + }; + + let canonical = method_admission + .as_ref() + .map(|admission| admission.canonical.as_ref()) + .unwrap_or(method_name); + let method_in_flight = method_admission.as_ref().and_then(|_| { + metrics.as_ref().map(|m| { + m.method_limit_admitted(canonical); + m.method_limit_in_flight(canonical) + }) + }); + + let mut is_rate_limited = false; + + if let Some(limit) = rate_limit.as_ref() { + let mut attempts = 0; + let jitter = Jitter::up_to(MAX_JITTER); + + loop { + if attempts >= MAX_RETRIES { + let rp = reject_too_many_calls(req.id.clone()); + if let Some(metrics) = metrics.as_ref() { + metrics.on_response(&req, &rp, true, now); + } + log_rpc_call( + trusted_client_ip, + protocol, + method_name, + canonical, + method_limiters.as_deref(), + now, + "limited", + "connection_rate", + ); + return rp; + } + + if let Err(rejected) = limit.inner.check() { + tokio::time::sleep(jitter + rejected.wait_time_from(limit.clock.now())) + .await; + } else { + break; + } + + is_rate_limited = true; + attempts += 1; + } + } + + let rp = service.call(req.clone()).await; + if let Some(metrics) = metrics.as_ref() { + metrics.on_response(&req, &rp, is_rate_limited, now); + } + log_rpc_call( + trusted_client_ip, + protocol, + method_name, + canonical, + method_limiters.as_deref(), + now, + if rp.is_success() { "success" } else { "error" }, + "none", + ); + + // Keep both the method admission and in-flight gauge guard alive until + // the service future has completed. + let _ = method_in_flight; + let _ = method_admission; + rp + } + .boxed() + } +} + +#[allow( + clippy::too_many_arguments, + reason = "The arguments form one structured RPC trace event" +)] +fn log_rpc_call( + trusted_client_ip: Option, + protocol: &'static str, + method: &str, + canonical: &str, + method_limiters: Option<&MethodLimiters>, + now: Instant, + outcome: &'static str, + limit_reason: &'static str, +) { + if !log::log_enabled!(target: "rpc_calls", log::Level::Trace) { + return; + } + + let concurrency = method_limiters.and_then(|limiters| limiters.concurrency(method)); + let method_limit_configured = concurrency.is_some(); + let (in_flight, max_in_flight) = concurrency.unwrap_or((0, 0)); + log::trace!( + target: "rpc_calls", + "protocol={protocol}, client_ip={trusted_client_ip:?}, method={method:.128?}, canonical={canonical:.128?}, duration_us={}, outcome={outcome}, limit_reason={limit_reason}, method_limit_configured={method_limit_configured}, in_flight={in_flight}, max_in_flight={max_in_flight}", + now.elapsed().as_micros(), + ); +} + +fn reject_method_limit(id: Id) -> MethodResponse { + MethodResponse::error( + id, + ErrorObject::owned(-32999, "RPC method limit exceeded", None::<()>), + ) +} + +fn reject_too_many_calls(id: Id) -> MethodResponse { + MethodResponse::error( + id, + ErrorObject::owned(-32999, "RPC rate limit exceeded", None::<()>), + ) +} +#[cfg(test)] +mod method_limit_tests { + use std::{ + borrow::Cow, + net::IpAddr, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, + task::Poll, + }; + + use futures::{future::BoxFuture, FutureExt}; + use jsonrpsee::{ + server::middleware::rpc::RpcServiceT, + types::{ErrorObject, Id, Request}, + MethodResponse, + }; + use tower::Layer; + + use super::{method_limit::MethodLimitReason, *}; + + #[derive(Clone)] + struct FakeService { + calls: Arc, + pending: bool, + } + + impl FakeService { + fn new(calls: Arc, pending: bool) -> Self { + Self { calls, pending } + } + } + + impl<'a> RpcServiceT<'a> for FakeService { + type Future = BoxFuture<'a, MethodResponse>; + + fn call(&self, req: Request<'a>) -> Self::Future { + self.calls.fetch_add(1, Ordering::SeqCst); + let id = req.id; + let pending = self.pending; + async move { + if pending { + futures::future::pending::<()>().await; + } + MethodResponse::error(id, ErrorObject::owned(-1, "service called", None::<()>)) + } + .boxed() + } + } + + fn request(method: &'static str, id: u64) -> Request<'static> { + Request::new(Cow::Borrowed(method), None, Id::Number(id)) + } + + fn limiters(specs: &[&str]) -> Arc { + Arc::new( + MethodLimiters::try_new( + specs + .iter() + .map(|spec| spec.parse().expect("valid method limit")) + .collect(), + ) + .unwrap(), + ) + } + + fn layer(limiters: Arc, service: S) -> Middleware { + MiddlewareLayer::new() + .with_method_limiters(limiters, "http", IpAddr::from([127, 0, 0, 1])) + .layer(service) + } + + #[tokio::test] + async fn method_limit_shared_registry_contends_across_layers() { + let limiters = limiters(&["state_getRuntimeVersion=100,1"]); + let first_calls = Arc::new(AtomicUsize::new(0)); + let second_calls = Arc::new(AtomicUsize::new(0)); + let first = layer( + limiters.clone(), + FakeService::new(first_calls.clone(), true), + ); + let second = layer(limiters, FakeService::new(second_calls.clone(), false)); + + let mut first_call = first.call(request("state_getRuntimeVersion", 1)); + assert!(matches!(futures::poll!(first_call.as_mut()), Poll::Pending)); + let response = second.call(request("state_getRuntimeVersion", 2)).await; + + assert_eq!(response.as_error_code(), Some(-32999)); + assert!(response.to_result().contains("RPC method limit exceeded")); + assert_eq!(first_calls.load(Ordering::SeqCst), 1); + assert_eq!(second_calls.load(Ordering::SeqCst), 0); + drop(first_call); + } + + #[tokio::test] + async fn method_limit_release_admits_after_immediate_rejection() { + let limiters = limiters(&["state_getRuntimeVersion=100,1"]); + let first = layer( + limiters.clone(), + FakeService::new(Arc::new(AtomicUsize::new(0)), true), + ); + let second_calls = Arc::new(AtomicUsize::new(0)); + let second = layer(limiters, FakeService::new(second_calls.clone(), false)); + + let mut first_call = first.call(request("state_getRuntimeVersion", 1)); + assert!(matches!(futures::poll!(first_call.as_mut()), Poll::Pending)); + assert_eq!( + second + .call(request("state_getRuntimeVersion", 2)) + .await + .as_error_code(), + Some(-32999) + ); + + drop(first_call); + assert_eq!( + second + .call(request("state_getRuntimeVersion", 3)) + .await + .as_error_code(), + Some(-1) + ); + assert_eq!(second_calls.load(Ordering::SeqCst), 1); + } + + #[tokio::test] + async fn method_limit_aliases_share_one_budget() { + let limiters = limiters(&["state_getRuntimeVersion,chain_getRuntimeVersion=100,1"]); + let first = layer( + limiters.clone(), + FakeService::new(Arc::new(AtomicUsize::new(0)), true), + ); + let alias_calls = Arc::new(AtomicUsize::new(0)); + let alias = layer(limiters, FakeService::new(alias_calls.clone(), false)); + + let mut canonical_call = first.call(request("state_getRuntimeVersion", 1)); + assert!(matches!( + futures::poll!(canonical_call.as_mut()), + Poll::Pending + )); + assert_eq!( + alias + .call(request("chain_getRuntimeVersion", 2)) + .await + .as_error_code(), + Some(-32999) + ); + + drop(canonical_call); + assert_eq!( + alias + .call(request("chain_getRuntimeVersion", 3)) + .await + .as_error_code(), + Some(-1) + ); + assert_eq!(alias_calls.load(Ordering::SeqCst), 1); + } + + #[test] + fn method_limit_groups_are_independent_and_unconfigured_unlimited() { + let limiters = limiters(&["alpha=100,1", "beta=100,1"]); + let alpha = limiters + .try_acquire("alpha") + .expect("configured") + .expect("admitted"); + assert!(matches!( + limiters.try_acquire("alpha"), + Err(MethodLimitReason::Concurrency) + )); + let beta = limiters + .try_acquire("beta") + .expect("configured") + .expect("admitted"); + assert!(limiters + .try_acquire("unconfigured") + .expect("unconfigured") + .is_none()); + drop(alpha); + drop(beta); + } + + #[tokio::test] + async fn method_limit_rate_rejection_skips_service() { + let limiters = limiters(&["state_getRuntimeVersion=1,2"]); + let calls = Arc::new(AtomicUsize::new(0)); + let service = layer(limiters, FakeService::new(calls.clone(), false)); + + assert_eq!( + service + .call(request("state_getRuntimeVersion", 1)) + .await + .as_error_code(), + Some(-1) + ); + let response = service.call(request("state_getRuntimeVersion", 2)).await; + assert_eq!(response.as_error_code(), Some(-32999)); + assert!(response.to_result().contains("RPC method limit exceeded")); + assert_eq!(calls.load(Ordering::SeqCst), 1); + } + + #[tokio::test] + async fn method_limit_active_without_old_rate_limiter_or_whitelist_bypass() { + let limiters = limiters(&["state_getRuntimeVersion=100,1"]); + let calls = Arc::new(AtomicUsize::new(0)); + let service = layer(limiters, FakeService::new(calls.clone(), true)); + + let mut first_call = service.call(request("state_getRuntimeVersion", 1)); + assert!(matches!(futures::poll!(first_call.as_mut()), Poll::Pending)); + let response = service.call(request("state_getRuntimeVersion", 2)).await; + + assert_eq!(response.as_error_code(), Some(-32999)); + assert_eq!(calls.load(Ordering::SeqCst), 1); + drop(first_call); + } + + #[tokio::test] + async fn method_limit_cancelling_admitted_call_releases_permit() { + let limiters = limiters(&["state_getRuntimeVersion=100,1"]); + let service = layer( + limiters.clone(), + FakeService::new(Arc::new(AtomicUsize::new(0)), true), + ); + + let mut call = service.call(request("state_getRuntimeVersion", 1)); + assert!(matches!(futures::poll!(call.as_mut()), Poll::Pending)); + drop(call); + + let admission = limiters + .try_acquire("state_getRuntimeVersion") + .expect("configured") + .expect("permit released on cancellation"); + drop(admission); + } + + #[tokio::test] + async fn method_limit_each_direct_request_is_charged() { + let limiters = limiters(&["state_getRuntimeVersion=100,1"]); + let service = layer( + limiters, + FakeService::new(Arc::new(AtomicUsize::new(0)), true), + ); + + let mut first_call = service.call(request("state_getRuntimeVersion", 1)); + assert!(matches!(futures::poll!(first_call.as_mut()), Poll::Pending)); + assert_eq!( + service + .call(request("state_getRuntimeVersion", 2)) + .await + .as_error_code(), + Some(-32999) + ); + drop(first_call); + } +} diff --git a/substrate/rpc-servers/src/middleware/node_health.rs b/substrate/rpc-servers/src/middleware/node_health.rs new file mode 100644 index 00000000000..cca3ae90af2 --- /dev/null +++ b/substrate/rpc-servers/src/middleware/node_health.rs @@ -0,0 +1,193 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Middleware for handling `/health` and `/health/readiness` endpoints. + +use std::{ + error::Error, + future::Future, + pin::Pin, + task::{Context, Poll}, +}; + +use futures::future::FutureExt; +use http::{HeaderValue, Method, StatusCode, Uri}; +use jsonrpsee::{ + server::{HttpBody, HttpRequest, HttpResponse}, + types::{Response as RpcResponse, ResponseSuccess as RpcResponseSuccess}, +}; +use tower::Service; + +const RPC_SYSTEM_HEALTH_CALL: &str = r#"{"jsonrpc":"2.0","method":"system_health","id":0}"#; +const HEADER_VALUE_JSON: HeaderValue = HeaderValue::from_static("application/json; charset=utf-8"); + +/// Layer that applies [`NodeHealthProxy`] which +/// proxies `/health` and `/health/readiness` endpoints. +#[derive(Debug, Clone, Default)] +pub struct NodeHealthProxyLayer; + +impl tower::Layer for NodeHealthProxyLayer { + type Service = NodeHealthProxy; + + fn layer(&self, service: S) -> Self::Service { + NodeHealthProxy::new(service) + } +} + +/// Middleware that proxies `/health` and `/health/readiness` endpoints. +pub struct NodeHealthProxy(S); + +impl NodeHealthProxy { + /// Creates a new [`NodeHealthProxy`]. + pub fn new(service: S) -> Self { + Self(service) + } +} + +impl tower::Service> for NodeHealthProxy +where + S: Service, + S::Response: 'static, + S::Error: Into> + 'static, + S::Future: Send + 'static, +{ + type Response = S::Response; + type Error = Box; + type Future = + Pin> + Send + 'static>>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.0.poll_ready(cx).map_err(Into::into) + } + + fn call(&mut self, req: http::Request) -> Self::Future { + let mut req = req.map(HttpBody::new); + let maybe_intercept = InterceptRequest::from_http(&req); + + // Modify the request and proxy it to `system_health` + if let InterceptRequest::Health | InterceptRequest::Readiness = maybe_intercept { + // RPC methods are accessed with `POST`. + *req.method_mut() = Method::POST; + // Precautionary remove the URI. + *req.uri_mut() = Uri::from_static("/"); + + // Requests must have the following headers: + req.headers_mut() + .insert(http::header::CONTENT_TYPE, HEADER_VALUE_JSON); + req.headers_mut() + .insert(http::header::ACCEPT, HEADER_VALUE_JSON); + + // Adjust the body to reflect the method call. + req = req.map(|_| HttpBody::from(RPC_SYSTEM_HEALTH_CALL)); + } + + // Call the inner service and get a future that resolves to the response. + let fut = self.0.call(req); + + async move { + let res = fut.await.map_err(Into::into)?; + + Ok(match maybe_intercept { + InterceptRequest::Deny => { + http_response(StatusCode::METHOD_NOT_ALLOWED, HttpBody::empty()) + } + InterceptRequest::No => res, + InterceptRequest::Health => { + let health = parse_rpc_response(res.into_body()).await?; + http_ok_response(serde_json::to_string(&health)?) + } + InterceptRequest::Readiness => { + let health = parse_rpc_response(res.into_body()).await?; + if (!health.is_syncing && health.peers > 0) || !health.should_have_peers { + http_ok_response(HttpBody::empty()) + } else { + http_internal_error() + } + } + }) + } + .boxed() + } +} + +// NOTE: This is duplicated here to avoid dependency to the `RPC API`. +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct Health { + /// Number of connected peers + pub peers: usize, + /// Is the node syncing + pub is_syncing: bool, + /// Should this node have any peers + /// + /// Might be false for local chains or when running without discovery. + pub should_have_peers: bool, +} + +fn http_ok_response>(body: S) -> HttpResponse { + http_response(StatusCode::OK, body) +} + +fn http_response>(status_code: StatusCode, body: S) -> HttpResponse { + HttpResponse::builder() + .status(status_code) + .header(http::header::CONTENT_TYPE, HEADER_VALUE_JSON) + .body(body.into()) + .expect("Header is valid; qed") +} + +fn http_internal_error() -> HttpResponse { + http_response(hyper::StatusCode::INTERNAL_SERVER_ERROR, HttpBody::empty()) +} + +async fn parse_rpc_response( + body: HttpBody, +) -> Result> { + use http_body_util::BodyExt; + + let bytes = body.collect().await?.to_bytes(); + + let raw_rp = serde_json::from_slice::>(&bytes)?; + let rp = RpcResponseSuccess::::try_from(raw_rp)?; + + Ok(rp.result) +} + +/// Whether the request should be treated as ordinary RPC call or be modified. +enum InterceptRequest { + /// Proxy `/health` to `system_health`. + Health, + /// Checks if node has at least one peer and is not doing major syncing. + /// + /// Returns HTTP status code 200 on success otherwise HTTP status code 500 is returned. + Readiness, + /// Treat as a ordinary RPC call and don't modify the request or response. + No, + /// Deny health or readiness calls that is not HTTP GET request. + /// + /// Returns HTTP status code 405. + Deny, +} + +impl InterceptRequest { + fn from_http(req: &HttpRequest) -> InterceptRequest { + match req.uri().path() { + "/health" => { + if req.method() == http::Method::GET { + InterceptRequest::Health + } else { + InterceptRequest::Deny + } + } + "/health/readiness" => { + if req.method() == http::Method::GET { + InterceptRequest::Readiness + } else { + InterceptRequest::Deny + } + } + // Forward all other requests to the RPC server. + _ => InterceptRequest::No, + } + } +} diff --git a/substrate/rpc-servers/src/middleware/rate_limit.rs b/substrate/rpc-servers/src/middleware/rate_limit.rs new file mode 100644 index 00000000000..f061baaaf27 --- /dev/null +++ b/substrate/rpc-servers/src/middleware/rate_limit.rs @@ -0,0 +1,35 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! RPC rate limit. + +use governor::{ + clock::{DefaultClock, QuantaClock}, + middleware::NoOpMiddleware, + state::{InMemoryState, NotKeyed}, + Quota, +}; +use std::{num::NonZeroU32, sync::Arc}; + +type RateLimitInner = governor::RateLimiter; + +/// Rate limit. +#[derive(Debug, Clone)] +pub struct RateLimit { + pub(crate) inner: Arc, + pub(crate) clock: QuantaClock, +} + +impl RateLimit { + /// Create a new `RateLimit` per minute. + pub fn per_minute(n: NonZeroU32) -> Self { + let clock = QuantaClock::default(); + Self { + inner: Arc::new(RateLimitInner::direct_with_clock( + Quota::per_minute(n), + &clock, + )), + clock, + } + } +} diff --git a/substrate/rpc-servers/src/utils.rs b/substrate/rpc-servers/src/utils.rs new file mode 100644 index 00000000000..d0126265fed --- /dev/null +++ b/substrate/rpc-servers/src/utils.rs @@ -0,0 +1,355 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Substrate RPC server utils. + +use crate::BatchRequestConfig; +use std::{ + error::Error as StdError, + net::{IpAddr, SocketAddr}, + num::NonZeroU32, + str::FromStr, +}; + +use forwarded_header_value::ForwardedHeaderValue; +use http::header::{HeaderName, HeaderValue}; +use ip_network::IpNetwork; +use jsonrpsee::{server::middleware::http::HostFilterLayer, RpcModule}; +use sc_rpc_api::DenyUnsafe; +use tower_http::cors::{AllowOrigin, CorsLayer}; + +const X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for"); +const X_REAL_IP: HeaderName = HeaderName::from_static("x-real-ip"); +const FORWARDED: HeaderName = HeaderName::from_static("forwarded"); + +#[derive(Debug)] +pub(crate) struct ListenAddrError; + +impl std::error::Error for ListenAddrError {} + +impl std::fmt::Display for ListenAddrError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "No listen address was successfully bound") + } +} + +/// Available RPC methods. +#[derive(Debug, Copy, Clone, Default)] +pub enum RpcMethods { + /// Allow only a safe subset of RPC methods. + Safe, + /// Expose every RPC method (even potentially unsafe ones). + Unsafe, + /// Automatically determine the RPC methods based on the connection. + #[default] + Auto, +} + +impl FromStr for RpcMethods { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "safe" => Ok(RpcMethods::Safe), + "unsafe" => Ok(RpcMethods::Unsafe), + "auto" => Ok(RpcMethods::Auto), + invalid => Err(format!("Invalid rpc methods {invalid}")), + } + } +} + +#[derive(Debug, Clone)] +pub(crate) struct RpcSettings { + pub(crate) batch_config: BatchRequestConfig, + pub(crate) max_connections: u32, + pub(crate) max_payload_in_mb: u32, + pub(crate) max_payload_out_mb: u32, + pub(crate) max_subscriptions_per_connection: u32, + pub(crate) max_buffer_capacity_per_connection: u32, + pub(crate) rpc_methods: RpcMethods, + pub(crate) rate_limit: Option, + pub(crate) rate_limit_trust_proxy_headers: bool, + pub(crate) rate_limit_whitelisted_ips: Vec, + pub(crate) cors: CorsLayer, + pub(crate) host_filter: Option, +} + +/// Represent a single RPC endpoint with its configuration. +#[derive(Debug, Clone)] +pub struct RpcEndpoint { + /// Listen address. + pub listen_addr: SocketAddr, + /// Batch request configuration. + pub batch_config: BatchRequestConfig, + /// Maximum number of connections. + pub max_connections: u32, + /// Maximum inbound payload size in MB. + pub max_payload_in_mb: u32, + /// Maximum outbound payload size in MB. + pub max_payload_out_mb: u32, + /// Maximum number of subscriptions per connection. + pub max_subscriptions_per_connection: u32, + /// Maximum buffer capacity per connection. + pub max_buffer_capacity_per_connection: u32, + /// Rate limit per minute. + pub rate_limit: Option, + /// Whether to trust proxy headers for rate limiting. + pub rate_limit_trust_proxy_headers: bool, + /// Whitelisted IPs for rate limiting. + pub rate_limit_whitelisted_ips: Vec, + /// CORS. + pub cors: Option>, + /// RPC methods to expose. + pub rpc_methods: RpcMethods, + /// Whether it's an optional listening address i.e, it's ignored if it fails to bind. + /// For example substrate tries to bind both ipv4 and ipv6 addresses but some platforms + /// may not support ipv6. + pub is_optional: bool, + /// Whether to retry with a random port if the provided port is already in use. + pub retry_random_port: bool, +} + +impl RpcEndpoint { + /// Binds to the listen address. + pub(crate) async fn bind(self) -> Result> { + let listener = match tokio::net::TcpListener::bind(self.listen_addr).await { + Ok(listener) => listener, + Err(_) if self.retry_random_port => { + let mut addr = self.listen_addr; + addr.set_port(0); + + tokio::net::TcpListener::bind(addr).await? + } + Err(e) => return Err(e.into()), + }; + let local_addr = listener.local_addr()?; + let host_filter = host_filtering(self.cors.is_some(), local_addr); + let cors = try_into_cors(self.cors)?; + + Ok(Listener { + listener, + local_addr, + cfg: RpcSettings { + batch_config: self.batch_config, + max_connections: self.max_connections, + max_payload_in_mb: self.max_payload_in_mb, + max_payload_out_mb: self.max_payload_out_mb, + max_subscriptions_per_connection: self.max_subscriptions_per_connection, + max_buffer_capacity_per_connection: self.max_buffer_capacity_per_connection, + rpc_methods: self.rpc_methods, + rate_limit: self.rate_limit, + rate_limit_trust_proxy_headers: self.rate_limit_trust_proxy_headers, + rate_limit_whitelisted_ips: self.rate_limit_whitelisted_ips, + host_filter, + cors, + }, + }) + } +} + +/// TCP socket server with RPC settings. +pub(crate) struct Listener { + listener: tokio::net::TcpListener, + local_addr: SocketAddr, + cfg: RpcSettings, +} + +impl Listener { + /// Accepts a new connection. + pub(crate) async fn accept(&mut self) -> std::io::Result<(tokio::net::TcpStream, SocketAddr)> { + let (sock, remote_addr) = self.listener.accept().await?; + Ok((sock, remote_addr)) + } + + /// Returns the local address the listener is bound to. + pub fn local_addr(&self) -> SocketAddr { + self.local_addr + } + + pub fn rpc_settings(&self) -> RpcSettings { + self.cfg.clone() + } +} + +pub(crate) fn host_filtering(enabled: bool, addr: SocketAddr) -> Option { + if enabled { + // NOTE: The listening addresses are whitelisted by default. + + let hosts = [ + format!("localhost:{}", addr.port()), + format!("127.0.0.1:{}", addr.port()), + format!("[::1]:{}", addr.port()), + ]; + + Some(HostFilterLayer::new(hosts).expect("Valid hosts; qed")) + } else { + None + } +} + +pub(crate) fn build_rpc_api(mut rpc_api: RpcModule) -> RpcModule { + let mut available_methods = rpc_api.method_names().collect::>(); + // The "rpc_methods" is defined below and we want it to be part of the reported methods. + available_methods.push("rpc_methods"); + available_methods.sort(); + + rpc_api + .register_method("rpc_methods", move |_, _, _| { + serde_json::json!({ + "methods": available_methods, + }) + }) + .expect("infallible all other methods have their own address space; qed"); + + rpc_api +} + +pub(crate) fn try_into_cors( + maybe_cors: Option>, +) -> Result> { + if let Some(cors) = maybe_cors { + let mut list = Vec::new(); + + for origin in cors { + list.push(HeaderValue::from_str(&origin)?) + } + + Ok(CorsLayer::new().allow_origin(AllowOrigin::list(list))) + } else { + // allow all cors + Ok(CorsLayer::permissive()) + } +} + +/// Extracts the IP addr from the HTTP request. +/// +/// It is extracted in the following order: +/// 1. `Forwarded` header. +/// 2. `X-Forwarded-For` header. +/// 3. `X-Real-Ip`. +pub(crate) fn get_proxy_ip(req: &http::Request) -> Option { + if let Some(ip) = req + .headers() + .get(&FORWARDED) + .and_then(|v| v.to_str().ok()) + .and_then(|v| ForwardedHeaderValue::from_forwarded(v).ok()) + .and_then(|v| v.remotest_forwarded_for_ip()) + { + return Some(ip); + } + + if let Some(ip) = req + .headers() + .get(&X_FORWARDED_FOR) + .and_then(|v| v.to_str().ok()) + .and_then(|v| ForwardedHeaderValue::from_x_forwarded_for(v).ok()) + .and_then(|v| v.remotest_forwarded_for_ip()) + { + return Some(ip); + } + + if let Some(ip) = req + .headers() + .get(&X_REAL_IP) + .and_then(|v| v.to_str().ok()) + .and_then(|v| IpAddr::from_str(v).ok()) + { + return Some(ip); + } + + None +} + +/// Get the `deny_unsafe` setting based on the address and the RPC methods exposed by the interface. +pub fn deny_unsafe(addr: &SocketAddr, methods: &RpcMethods) -> DenyUnsafe { + match (addr.ip().is_loopback(), methods) { + (_, RpcMethods::Unsafe) | (true, RpcMethods::Auto) => DenyUnsafe::No, + _ => DenyUnsafe::Yes, + } +} + +pub(crate) fn format_listen_addrs(addr: &[SocketAddr]) -> String { + let mut s = String::new(); + + let mut it = addr.iter().peekable(); + + while let Some(addr) = it.next() { + s.push_str(&addr.to_string()); + + if it.peek().is_some() { + s.push(','); + } + } + + if addr.len() == 1 { + s.push(','); + } + + s +} + +#[cfg(test)] +mod tests { + use super::*; + use hyper::header::HeaderValue; + use jsonrpsee::server::{HttpBody, HttpRequest}; + + fn request() -> http::Request { + HttpRequest::builder().body(HttpBody::empty()).unwrap() + } + + #[test] + fn empty_works() { + let req = request(); + let host = get_proxy_ip(&req); + assert!(host.is_none()) + } + + #[test] + fn host_from_x_real_ip() { + let mut req = request(); + + req.headers_mut() + .insert(&X_REAL_IP, HeaderValue::from_static("127.0.0.1")); + let ip = get_proxy_ip(&req); + assert_eq!(Some(IpAddr::from_str("127.0.0.1").unwrap()), ip); + } + + #[test] + fn ip_from_forwarded_works() { + let mut req = request(); + + req.headers_mut().insert( + &FORWARDED, + HeaderValue::from_static("for=192.0.2.60;proto=http;by=203.0.113.43;host=example.com"), + ); + let ip = get_proxy_ip(&req); + assert_eq!(Some(IpAddr::from_str("192.0.2.60").unwrap()), ip); + } + + #[test] + fn ip_from_forwarded_multiple() { + let mut req = request(); + + req.headers_mut() + .append(&FORWARDED, HeaderValue::from_static("for=127.0.0.1")); + req.headers_mut() + .append(&FORWARDED, HeaderValue::from_static("for=192.0.2.60")); + req.headers_mut() + .append(&FORWARDED, HeaderValue::from_static("for=192.0.2.61")); + let ip = get_proxy_ip(&req); + assert_eq!(Some(IpAddr::from_str("127.0.0.1").unwrap()), ip); + } + + #[test] + fn ip_from_x_forwarded_works() { + let mut req = request(); + + req.headers_mut().insert( + &X_FORWARDED_FOR, + HeaderValue::from_static("127.0.0.1,192.0.2.60,0.0.0.1"), + ); + let ip = get_proxy_ip(&req); + assert_eq!(Some(IpAddr::from_str("127.0.0.1").unwrap()), ip); + } +} diff --git a/substrate/runtime-executor/Cargo.toml b/substrate/runtime-executor/Cargo.toml index 6845a997dac..fda9905ba92 100644 --- a/substrate/runtime-executor/Cargo.toml +++ b/substrate/runtime-executor/Cargo.toml @@ -53,6 +53,9 @@ sp-version.default-features = true sp-wasm-interface.workspace = true sp-wasm-interface.default-features = true +[dev-dependencies] +wat.workspace = true + [features] default = ["std"] # This crate does not have `no_std` support, we just require this for tests diff --git a/substrate/runtime-executor/src/executor.rs b/substrate/runtime-executor/src/executor.rs index d81fe97c7cd..29bbc0ebfe8 100644 --- a/substrate/runtime-executor/src/executor.rs +++ b/substrate/runtime-executor/src/executor.rs @@ -546,6 +546,12 @@ where }) .unwrap_or_else(|| self.default_onchain_heap_alloc_strategy) }; + if let Some(result) = + self.cache + .cached_runtime_version(runtime_code, self.method, on_chain_heap_pages) + { + return result; + } self.with_instance( runtime_code, @@ -763,3 +769,65 @@ impl sp_core::traits::ReadRuntimeVersion for NativeE self.wasm.read_runtime_version(wasm_code, ext) } } +#[cfg(test)] +mod tests { + use super::WasmExecutor; + use crate::{ + RuntimeVersionOf, + error::Error, + wasm_runtime::tests::{insert_cached_runtime, runtime_code}, + }; + use sp_io::TestExternalities; + use sp_version::RuntimeVersion; + use std::sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }; + + #[test] + fn cached_runtime_version_skips_instance_allocation() { + let executor = WasmExecutor::::builder().build(); + let method = executor.method; + let heap_alloc_strategy = executor.default_onchain_heap_alloc_strategy; + + let known_code = runtime_code(&[1, 2, 3]); + let known_calls = Arc::new(AtomicUsize::new(0)); + let expected = RuntimeVersion { + spec_name: "cached".into(), + ..Default::default() + }; + insert_cached_runtime( + &executor.cache, + &known_code.hash, + method, + heap_alloc_strategy, + Some(expected.clone()), + known_calls.clone(), + ); + + let unknown_code = runtime_code(&[4, 5, 6]); + let unknown_calls = Arc::new(AtomicUsize::new(0)); + insert_cached_runtime( + &executor.cache, + &unknown_code.hash, + method, + heap_alloc_strategy, + None, + unknown_calls.clone(), + ); + + let mut ext = TestExternalities::default(); + let actual = RuntimeVersionOf::runtime_version(&executor, &mut ext.ext(), &known_code) + .expect("cached runtime version should be returned"); + assert_eq!(actual, expected); + assert_eq!(known_calls.load(Ordering::SeqCst), 0); + + let error = RuntimeVersionOf::runtime_version(&executor, &mut ext.ext(), &unknown_code) + .expect_err("cached missing runtime version should remain unknown"); + match error { + Error::ApiError(message) => assert_eq!(message.to_string(), "Unknown version"), + other => panic!("expected unknown version error, got {other:?}"), + } + assert_eq!(unknown_calls.load(Ordering::SeqCst), 0); + } +} diff --git a/substrate/runtime-executor/src/wasm_runtime.rs b/substrate/runtime-executor/src/wasm_runtime.rs index 87a74c2e1f4..185a1c1263b 100644 --- a/substrate/runtime-executor/src/wasm_runtime.rs +++ b/substrate/runtime-executor/src/wasm_runtime.rs @@ -178,6 +178,31 @@ impl RuntimeCache { } } + /// Returns the cached runtime version without acquiring an instance. + pub(crate) fn cached_runtime_version( + &self, + runtime_code: &RuntimeCode, + wasm_method: WasmExecutionMethod, + heap_alloc_strategy: HeapAllocStrategy, + ) -> Option> { + let versioned_runtime_id = VersionedRuntimeId { + code_hash: runtime_code.hash.clone(), + wasm_method, + heap_alloc_strategy, + }; + + let mut runtimes = self.runtimes.lock(); + let runtime = runtimes.get(&versioned_runtime_id).cloned(); + drop(runtimes); + + runtime.map(|runtime| { + runtime + .version + .clone() + .ok_or_else(|| Error::ApiError("Unknown version".into())) + }) + } + /// Prepares a WASM module instance and executes given function for it. /// /// This uses internal cache to find available instance or create a new one. @@ -437,3 +462,224 @@ where instances, }) } +#[cfg(test)] +pub(crate) mod tests { + use super::{ + Error, RuntimeCache, RuntimeVersion, VersionedRuntime, VersionedRuntimeId, + WasmExecutionMethod, + }; + use crate::{RuntimeVersionOf, executor::WasmExecutor}; + use codec::Encode; + use sc_executor_common::wasm_runtime::{HeapAllocStrategy, WasmInstance, WasmModule}; + use sp_core::traits::{RuntimeCode, WrappedRuntimeCode}; + use sp_io::TestExternalities; + use std::{ + borrow::Cow, + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, + }; + + struct CountingModule { + new_instance_calls: Arc, + } + + impl WasmModule for CountingModule { + fn new_instance(&self) -> Result, Error> { + self.new_instance_calls.fetch_add(1, Ordering::SeqCst); + Err(Error::Other("new_instance called".into())) + } + } + + pub(crate) fn runtime_code(hash: &[u8]) -> RuntimeCode<'static> { + let mut runtime_code = RuntimeCode::empty(); + runtime_code.hash = hash.to_vec(); + runtime_code + } + + pub(crate) fn insert_cached_runtime( + cache: &RuntimeCache, + code_hash: &[u8], + wasm_method: WasmExecutionMethod, + heap_alloc_strategy: HeapAllocStrategy, + version: Option, + new_instance_calls: Arc, + ) { + cache.runtimes.lock().insert( + VersionedRuntimeId { + code_hash: code_hash.to_vec(), + wasm_method, + heap_alloc_strategy, + }, + Arc::new(VersionedRuntime { + module: Box::new(CountingModule { new_instance_calls }), + version, + instances: Vec::new(), + }), + ); + } + + #[test] + fn cached_runtime_version_returns_known_version_without_new_instance() { + let cache = RuntimeCache::new(1, None, 1); + let code = runtime_code(&[1, 2, 3]); + let wasm_method = WasmExecutionMethod::default(); + let heap_alloc_strategy = HeapAllocStrategy::Static { extra_pages: 1 }; + let new_instance_calls = Arc::new(AtomicUsize::new(0)); + let version = RuntimeVersion { + spec_name: "cached".into(), + ..Default::default() + }; + + insert_cached_runtime( + &cache, + &code.hash, + wasm_method, + heap_alloc_strategy, + Some(version.clone()), + new_instance_calls.clone(), + ); + + match cache.cached_runtime_version(&code, wasm_method, heap_alloc_strategy) { + Some(Ok(actual)) => assert_eq!(actual, version), + other => panic!("expected cached runtime version, got {other:?}"), + } + assert_eq!(new_instance_calls.load(Ordering::SeqCst), 0); + } + + #[test] + fn cached_runtime_version_reports_unknown_without_new_instance() { + let cache = RuntimeCache::new(1, None, 1); + let code = runtime_code(&[1, 2, 3]); + let wasm_method = WasmExecutionMethod::default(); + let heap_alloc_strategy = HeapAllocStrategy::Static { extra_pages: 1 }; + let new_instance_calls = Arc::new(AtomicUsize::new(0)); + + insert_cached_runtime( + &cache, + &code.hash, + wasm_method, + heap_alloc_strategy, + None, + new_instance_calls.clone(), + ); + + let error = cache + .cached_runtime_version(&code, wasm_method, heap_alloc_strategy) + .expect("expected an exact cache hit") + .expect_err("expected cached absence to report an unknown version"); + match error { + Error::ApiError(message) => assert_eq!(message.to_string(), "Unknown version"), + other => panic!("expected unknown version error, got {other:?}"), + } + assert_eq!(new_instance_calls.load(Ordering::SeqCst), 0); + } + + #[test] + fn cached_runtime_version_requires_exact_cache_key() { + let cache = RuntimeCache::new(1, None, 1); + let code = runtime_code(&[1, 2, 3]); + let wasm_method = WasmExecutionMethod::default(); + let other_wasm_method = WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::RecreateInstance, + }; + let heap_alloc_strategy = HeapAllocStrategy::Static { extra_pages: 1 }; + let other_heap_alloc_strategy = HeapAllocStrategy::Dynamic { + maximum_pages: Some(1), + }; + let new_instance_calls = Arc::new(AtomicUsize::new(0)); + + insert_cached_runtime( + &cache, + &code.hash, + wasm_method, + heap_alloc_strategy, + Some(Default::default()), + new_instance_calls.clone(), + ); + + assert!( + cache + .cached_runtime_version(&runtime_code(&[4, 5, 6]), wasm_method, heap_alloc_strategy) + .is_none() + ); + assert!( + cache + .cached_runtime_version(&code, other_wasm_method, heap_alloc_strategy) + .is_none() + ); + assert!( + cache + .cached_runtime_version(&code, wasm_method, other_heap_alloc_strategy) + .is_none() + ); + assert_eq!(new_instance_calls.load(Ordering::SeqCst), 0); + } + #[test] + fn runtime_version_miss_returns_embedded_version() { + let wasm = wat::parse_str(r#"(module (memory (export "memory") 1))"#) + .expect("minimal WAT should parse"); + let expected = RuntimeVersion { + spec_name: "embedded-spec".into(), + impl_name: "embedded-impl".into(), + transaction_version: 1, + ..Default::default() + }; + let wasm = sp_version::embed::embed_runtime_version(&wasm, expected.clone()) + .expect("runtime version should embed"); + let code_fetcher = WrappedRuntimeCode(Cow::Owned(wasm)); + let runtime_code = RuntimeCode { + code_fetcher: &code_fetcher, + heap_pages: None, + hash: vec![0xeb, 0xed, 0x00, 0x01], + }; + let executor = WasmExecutor::::builder().build(); + let mut ext = TestExternalities::default(); + let actual = RuntimeVersionOf::runtime_version(&executor, &mut ext.ext(), &runtime_code) + .expect("embedded runtime version should be returned"); + + assert_eq!(actual, expected); + } + + #[test] + fn runtime_version_miss_returns_legacy_core_version() { + let expected = RuntimeVersion { + spec_name: "legacy-spec".into(), + impl_name: "legacy-impl".into(), + transaction_version: 1, + ..Default::default() + }; + let encoded = expected.encode(); + let data = encoded + .iter() + .map(|byte| format!(r#"\{byte:02x}"#)) + .collect::(); + let wat = format!( + r#"(module + (memory (export "memory") 1) + (global (export "__heap_base") i32 (i32.const 1024)) + (data (i32.const 0) "{data}") + (func (export "Core_version") (param i32 i32) (result i64) + i64.const {} + ) + )"#, + (encoded.len() as u64) << 32, + ); + let wasm = wat::parse_str(wat).expect("valid legacy runtime WAT"); + let code = WrappedRuntimeCode(Cow::Owned(wasm)); + let runtime_code = RuntimeCode { + code_fetcher: &code, + heap_pages: None, + hash: vec![0xde, 0xad, 0xbe, 0xef], + }; + let executor = crate::WasmExecutor::::builder().build(); + let mut ext = sp_io::TestExternalities::default(); + + let actual = + crate::RuntimeVersionOf::runtime_version(&executor, &mut ext.ext(), &runtime_code) + .expect("legacy Core_version decodes"); + + assert_eq!(actual, expected); + } +} diff --git a/substrate/service/Cargo.toml b/substrate/service/Cargo.toml new file mode 100644 index 00000000000..5eaa23ab9b7 --- /dev/null +++ b/substrate/service/Cargo.toml @@ -0,0 +1,127 @@ +[package] +name = "sc-service" +version = "0.46.0" +authors.workspace = true +edition = "2021" +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" +homepage.workspace = true +repository.workspace = true +description = "Substrate service. Starts a thread that spins up the network, client, and extrinsic pool. Manages communication between them." +readme = "README.md" + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[features] +default = ["rocksdb"] +# The RocksDB feature activates the RocksDB database backend. If it is not activated, and you pass +# a path to a database, an error will be produced at runtime. +rocksdb = ["sc-client-db/rocksdb"] +# exposes the client type +test-helpers = [] +runtime-benchmarks = [ + "sc-client-db/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] + +[dependencies] +jsonrpsee = { features = ["server"], workspace = true } +thiserror = { workspace = true } +futures = { workspace = true, default-features = true } +rand = { workspace = true, default-features = true } +parking_lot = { workspace = true, default-features = true } +log = { workspace = true, default-features = true } +futures-timer = { workspace = true } +exit-future = { workspace = true } +pin-project = { workspace = true } +serde = { workspace = true, default-features = true } +serde_json = { workspace = true, default-features = true } +sc-keystore.workspace = true +sc-keystore.default-features = true +sp-runtime.workspace = true +sp-runtime.default-features = true +sp-trie.workspace = true +sp-trie.default-features = true +sp-externalities.workspace = true +sp-externalities.default-features = true +sc-utils.workspace = true +sc-utils.default-features = true +sp-version.workspace = true +sp-version.default-features = true +sp-blockchain.workspace = true +sp-blockchain.default-features = true +sp-core.workspace = true +sp-core.default-features = true +sp-keystore.workspace = true +sp-keystore.default-features = true +sp-session.workspace = true +sp-session.default-features = true +sp-state-machine.workspace = true +sp-state-machine.default-features = true +sp-consensus.workspace = true +sp-consensus.default-features = true +sc-consensus.workspace = true +sc-consensus.default-features = true +sp-storage.workspace = true +sp-storage.default-features = true +sc-network.workspace = true +sc-network.default-features = true +sc-network-common.workspace = true +sc-network-common.default-features = true +sc-network-light.workspace = true +sc-network-light.default-features = true +sc-network-sync.workspace = true +sc-network-sync.default-features = true +sc-network-types.workspace = true +sc-network-types.default-features = true +sc-network-transactions.workspace = true +sc-network-transactions.default-features = true +sc-chain-spec.workspace = true +sc-chain-spec.default-features = true +sc-client-api.workspace = true +sc-client-api.default-features = true +sp-api.workspace = true +sp-api.default-features = true +sc-client-db.workspace = true +codec = { workspace = true, default-features = true } +sc-executor.workspace = true +sc-executor.default-features = true +sc-transaction-pool.workspace = true +sc-transaction-pool.default-features = true +sp-transaction-pool.workspace = true +sp-transaction-pool.default-features = true +sc-transaction-pool-api.workspace = true +sc-transaction-pool-api.default-features = true +sp-transaction-storage-proof.workspace = true +sp-transaction-storage-proof.default-features = true +sc-rpc-server.workspace = true +sc-rpc-server.default-features = true +sc-rpc.workspace = true +sc-rpc.default-features = true +sc-rpc-spec-v2.workspace = true +sc-rpc-spec-v2.default-features = true +sc-informant.workspace = true +sc-informant.default-features = true +sc-telemetry.workspace = true +sc-telemetry.default-features = true +prometheus-endpoint.workspace = true +prometheus-endpoint.default-features = true +sc-tracing.workspace = true +sc-tracing.default-features = true +sc-sysinfo.workspace = true +sc-sysinfo.default-features = true +tracing = { workspace = true, default-features = true } +tracing-futures = { workspace = true } +async-trait = { workspace = true } +tokio = { features = ["parking_lot", "rt-multi-thread", "time"], workspace = true, default-features = true } +tempfile = { workspace = true } +directories = { workspace = true } +static_init = { workspace = true } +schnellru = { workspace = true } + +[dev-dependencies] +substrate-test-runtime-client.workspace = true +substrate-test-runtime.workspace = true diff --git a/substrate/service/README.md b/substrate/service/README.md new file mode 100644 index 00000000000..c0c3b811c2f --- /dev/null +++ b/substrate/service/README.md @@ -0,0 +1,9 @@ +Substrate service. Starts a thread that spins up the network, client, and extrinsic pool. +Manages communication between them. + +License: GPL-3.0-or-later WITH Classpath-exception-2.0 + + +## Release + +Polkadot SDK stable2409 diff --git a/substrate/service/src/builder.rs b/substrate/service/src/builder.rs new file mode 100644 index 00000000000..7c97b66b431 --- /dev/null +++ b/substrate/service/src/builder.rs @@ -0,0 +1,1188 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::{ + build_network_future, build_system_rpc_future, + client::{Client, ClientConfig}, + config::{Configuration, ExecutorConfiguration, KeystoreConfig, PrometheusConfig}, + error::Error, + metrics::MetricsService, + start_rpc_servers, BuildGenesisBlock, GenesisBlockBuilder, RpcHandlers, SpawnTaskHandle, + TaskManager, TransactionPoolAdapter, +}; +use futures::{channel::oneshot, future::ready, FutureExt, StreamExt}; +use jsonrpsee::RpcModule; +use log::info; +use prometheus_endpoint::Registry; +use sc_chain_spec::{get_extension, ChainSpec}; +use sc_client_api::{ + execution_extensions::ExecutionExtensions, proof_provider::ProofProvider, BadBlocks, + BlockBackend, BlockchainEvents, ExecutorProvider, ForkBlocks, StorageProvider, UsageProvider, +}; +use sc_client_db::{Backend, BlocksPruning, DatabaseSettings, PruningMode}; +use sc_consensus::import_queue::ImportQueue; +use sc_executor::{ + sp_wasm_interface::HostFunctions, HeapAllocStrategy, NativeExecutionDispatch, RuntimeVersionOf, + WasmExecutor, DEFAULT_HEAP_ALLOC_STRATEGY, +}; +use sc_keystore::LocalKeystore; +use sc_network::{ + config::{FullNetworkConfiguration, SyncMode}, + service::{ + traits::{PeerStore, RequestResponseConfig}, + NotificationMetrics, + }, + NetworkBackend, NetworkStateInfo, +}; +use sc_network_common::role::Roles; +use sc_network_light::light_client_requests::handler::LightClientRequestHandler; +use sc_network_sync::{ + block_relay_protocol::BlockRelayParams, block_request_handler::BlockRequestHandler, + engine::SyncingEngine, service::network::NetworkServiceProvider, + state_request_handler::StateRequestHandler, + warp_request_handler::RequestHandler as WarpSyncRequestHandler, SyncingService, WarpSyncConfig, +}; +use sc_rpc::{ + author::AuthorApiServer, + chain::ChainApiServer, + offchain::OffchainApiServer, + state::{ChildStateApiServer, StateApiServer}, + system::SystemApiServer, + DenyUnsafe, SubscriptionTaskExecutor, +}; +use sc_rpc_spec_v2::{ + archive::ArchiveApiServer, + chain_head::ChainHeadApiServer, + chain_spec::ChainSpecApiServer, + transaction::{TransactionApiServer, TransactionBroadcastApiServer}, +}; +use sc_telemetry::{telemetry, ConnectionMessage, Telemetry, TelemetryHandle, SUBSTRATE_INFO}; +use sc_transaction_pool_api::{MaintainedTransactionPool, TransactionPool}; +use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedSender}; +use sp_api::{CallApiAt, ProvideRuntimeApi}; +use sp_blockchain::{HeaderBackend, HeaderMetadata}; +use sp_consensus::block_validation::{ + BlockAnnounceValidator, Chain, DefaultBlockAnnounceValidator, +}; +use sp_core::traits::{CodeExecutor, SpawnNamed}; +use sp_keystore::KeystorePtr; +use sp_runtime::traits::{Block as BlockT, BlockIdTo, NumberFor, Zero}; +use std::{str::FromStr, sync::Arc, time::SystemTime}; + +/// Full client type. +pub type TFullClient = + Client, TFullCallExecutor, TBl, TRtApi>; + +/// Full client backend type. +pub type TFullBackend = Backend; + +/// Full client call executor type. +pub type TFullCallExecutor = crate::client::LocalCallExecutor, TExec>; + +type TFullParts = ( + TFullClient, + Arc>, + KeystoreContainer, + TaskManager, +); + +/// Construct a local keystore shareable container +pub struct KeystoreContainer(Arc); + +impl KeystoreContainer { + /// Construct KeystoreContainer + pub fn new(config: &KeystoreConfig) -> Result { + let keystore = Arc::new(match config { + KeystoreConfig::Path { path, password } => { + LocalKeystore::open(path.clone(), password.clone())? + } + KeystoreConfig::InMemory => LocalKeystore::in_memory(), + }); + + Ok(Self(keystore)) + } + + /// Returns a shared reference to a dynamic `Keystore` trait implementation. + pub fn keystore(&self) -> KeystorePtr { + self.0.clone() + } + + /// Returns a shared reference to the local keystore . + pub fn local_keystore(&self) -> Arc { + self.0.clone() + } +} + +/// Creates a new full client for the given config. +pub fn new_full_client( + config: &Configuration, + telemetry: Option, + executor: TExec, +) -> Result, Error> +where + TBl: BlockT, + TExec: CodeExecutor + RuntimeVersionOf + Clone, +{ + new_full_parts(config, telemetry, executor).map(|parts| parts.0) +} + +/// Create the initial parts of a full node with the default genesis block builder. +pub fn new_full_parts_record_import( + config: &Configuration, + telemetry: Option, + executor: TExec, + enable_import_proof_recording: bool, +) -> Result, Error> +where + TBl: BlockT, + TExec: CodeExecutor + RuntimeVersionOf + Clone, +{ + let backend = new_db_backend(config.db_config())?; + + let genesis_block_builder = GenesisBlockBuilder::new( + config.chain_spec.as_storage_builder(), + !config.no_genesis(), + backend.clone(), + executor.clone(), + )?; + + new_full_parts_with_genesis_builder( + config, + telemetry, + executor, + backend, + genesis_block_builder, + enable_import_proof_recording, + ) +} +/// Create the initial parts of a full node with the default genesis block builder. +pub fn new_full_parts( + config: &Configuration, + telemetry: Option, + executor: TExec, +) -> Result, Error> +where + TBl: BlockT, + TExec: CodeExecutor + RuntimeVersionOf + Clone, +{ + new_full_parts_record_import(config, telemetry, executor, false) +} + +/// Create the initial parts of a full node. +pub fn new_full_parts_with_genesis_builder( + config: &Configuration, + telemetry: Option, + executor: TExec, + backend: Arc>, + genesis_block_builder: TBuildGenesisBlock, + enable_import_proof_recording: bool, +) -> Result, Error> +where + TBl: BlockT, + TExec: CodeExecutor + RuntimeVersionOf + Clone, + TBuildGenesisBlock: BuildGenesisBlock< + TBl, + BlockImportOperation = as sc_client_api::backend::Backend>::BlockImportOperation + >, +{ + let keystore_container = KeystoreContainer::new(&config.keystore)?; + + let task_manager = { + let registry = config.prometheus_config.as_ref().map(|cfg| &cfg.registry); + TaskManager::new(config.tokio_handle.clone(), registry)? + }; + + let chain_spec = &config.chain_spec; + let fork_blocks = get_extension::>(chain_spec.extensions()) + .cloned() + .unwrap_or_default(); + + let bad_blocks = get_extension::>(chain_spec.extensions()) + .cloned() + .unwrap_or_default(); + + let client = { + let extensions = ExecutionExtensions::new(None, Arc::new(executor.clone())); + + let wasm_runtime_substitutes = config + .chain_spec + .code_substitutes() + .into_iter() + .map(|(n, c)| { + let number = NumberFor::::from_str(&n).map_err(|_| { + Error::Application(Box::from(format!( + "Failed to parse `{}` as block number for code substitutes. \ + In an old version the key for code substitute was a block hash. \ + Please update the chain spec to a version that is compatible with your node.", + n + ))) + })?; + Ok((number, c)) + }) + .collect::, Error>>()?; + + let client = new_client( + backend.clone(), + executor, + genesis_block_builder, + fork_blocks, + bad_blocks, + extensions, + Box::new(task_manager.spawn_handle()), + config + .prometheus_config + .as_ref() + .map(|config| config.registry.clone()), + telemetry, + ClientConfig { + offchain_worker_enabled: config.offchain_worker.enabled, + offchain_indexing_api: config.offchain_worker.indexing_enabled, + wasm_runtime_overrides: config.wasm_runtime_overrides.clone(), + no_genesis: config.no_genesis(), + wasm_runtime_substitutes, + enable_import_proof_recording, + }, + )?; + + client + }; + + Ok((client, backend, keystore_container, task_manager)) +} + +/// Creates a [`NativeElseWasmExecutor`](sc_executor::NativeElseWasmExecutor) according to +/// [`Configuration`]. +#[deprecated(note = "Please switch to `new_wasm_executor`. Will be removed at end of 2024.")] +#[allow(deprecated)] +pub fn new_native_or_wasm_executor( + config: &Configuration, +) -> sc_executor::NativeElseWasmExecutor { + #[allow(deprecated)] + sc_executor::NativeElseWasmExecutor::new_with_wasm_executor(new_wasm_executor(&config.executor)) +} + +/// Creates a [`WasmExecutor`] according to [`ExecutorConfiguration`]. +pub fn new_wasm_executor(config: &ExecutorConfiguration) -> WasmExecutor { + let strategy = config + .default_heap_pages + .map_or(DEFAULT_HEAP_ALLOC_STRATEGY, |p| HeapAllocStrategy::Static { + extra_pages: p as _, + }); + WasmExecutor::::builder() + .with_execution_method(config.wasm_method) + .with_onchain_heap_alloc_strategy(strategy) + .with_offchain_heap_alloc_strategy(strategy) + .with_max_runtime_instances(config.max_runtime_instances) + .with_runtime_cache_size(config.runtime_cache_size) + .build() +} + +/// Create an instance of default DB-backend backend. +pub fn new_db_backend( + settings: DatabaseSettings, +) -> Result>, sp_blockchain::Error> +where + Block: BlockT, +{ + const CANONICALIZATION_DELAY: u64 = 4096; + + Ok(Arc::new(Backend::new(settings, CANONICALIZATION_DELAY)?)) +} + +/// Create an instance of client backed by given backend. +pub fn new_client( + backend: Arc>, + executor: E, + genesis_block_builder: G, + fork_blocks: ForkBlocks, + bad_blocks: BadBlocks, + execution_extensions: ExecutionExtensions, + spawn_handle: Box, + prometheus_registry: Option, + telemetry: Option, + config: ClientConfig, +) -> Result< + Client< + Backend, + crate::client::LocalCallExecutor, E>, + Block, + RA, + >, + sp_blockchain::Error, +> +where + Block: BlockT, + E: CodeExecutor + RuntimeVersionOf, + G: BuildGenesisBlock< + Block, + BlockImportOperation = as sc_client_api::backend::Backend>::BlockImportOperation + >, +{ + let executor = crate::client::LocalCallExecutor::new( + backend.clone(), + executor, + config.clone(), + execution_extensions, + )?; + + Client::new( + backend, + executor, + spawn_handle, + genesis_block_builder, + fork_blocks, + bad_blocks, + prometheus_registry, + telemetry, + config, + ) +} + +/// Parameters to pass into `build`. +pub struct SpawnTasksParams<'a, TBl: BlockT, TCl, TExPool, TRpc, Backend> { + /// The service configuration. + pub config: Configuration, + /// A shared client returned by `new_full_parts`. + pub client: Arc, + /// A shared backend returned by `new_full_parts`. + pub backend: Arc, + /// A task manager returned by `new_full_parts`. + pub task_manager: &'a mut TaskManager, + /// A shared keystore returned by `new_full_parts`. + pub keystore: KeystorePtr, + /// A shared transaction pool. + pub transaction_pool: Arc, + /// Builds additional [`RpcModule`]s that should be added to the server + pub rpc_builder: Box Result, Error>>, + /// A shared network instance. + pub network: Arc, + /// A Sender for RPC requests. + pub system_rpc_tx: TracingUnboundedSender>, + /// Controller for transactions handlers + pub tx_handler_controller: + sc_network_transactions::TransactionsHandlerController<::Hash>, + /// Syncing service. + pub sync_service: Arc>, + /// Telemetry instance for this node. + pub telemetry: Option<&'a mut Telemetry>, +} + +/// Spawn the tasks that are required to run a node. +pub fn spawn_tasks( + params: SpawnTasksParams, +) -> Result +where + TCl: ProvideRuntimeApi + + HeaderMetadata + + Chain + + BlockBackend + + BlockIdTo + + ProofProvider + + HeaderBackend + + BlockchainEvents + + ExecutorProvider + + UsageProvider + + StorageProvider + + CallApiAt + + Send + + 'static, + >::Api: sp_api::Metadata + + sp_transaction_pool::runtime_api::TaggedTransactionQueue + + sp_session::SessionKeys + + sp_api::ApiExt, + TBl: BlockT, + TBl::Hash: Unpin, + TBl::Header: Unpin, + TBackend: 'static + sc_client_api::backend::Backend + Send, + TExPool: MaintainedTransactionPool::Hash> + 'static, +{ + let SpawnTasksParams { + mut config, + task_manager, + client, + backend, + keystore, + transaction_pool, + rpc_builder, + network, + system_rpc_tx, + tx_handler_controller, + sync_service, + telemetry, + } = params; + + let chain_info = client.usage_info().chain; + + sp_session::generate_initial_session_keys( + client.clone(), + chain_info.best_hash, + config + .dev_key_seed + .clone() + .map(|s| vec![s]) + .unwrap_or_default(), + keystore.clone(), + ) + .map_err(|e| Error::Application(Box::new(e)))?; + + let sysinfo = sc_sysinfo::gather_sysinfo(); + sc_sysinfo::print_sysinfo(&sysinfo); + + let telemetry = telemetry + .map(|telemetry| { + init_telemetry( + config.network.node_name.clone(), + config.impl_name.clone(), + config.impl_version.clone(), + config.chain_spec.name().to_string(), + config.role.is_authority(), + network.clone(), + client.clone(), + telemetry, + Some(sysinfo), + ) + }) + .transpose()?; + + info!("📦 Highest known block at #{}", chain_info.best_number); + + let spawn_handle = task_manager.spawn_handle(); + + // Inform the tx pool about imported and finalized blocks. + spawn_handle.spawn( + "txpool-notifications", + Some("transaction-pool"), + sc_transaction_pool::notification_future(client.clone(), transaction_pool.clone()), + ); + + spawn_handle.spawn( + "on-transaction-imported", + Some("transaction-pool"), + propagate_transaction_notifications( + transaction_pool.clone(), + tx_handler_controller, + telemetry.clone(), + ), + ); + + // Prometheus metrics. + let metrics_service = + if let Some(PrometheusConfig { port, registry }) = config.prometheus_config.clone() { + // Set static metrics. + let metrics = MetricsService::with_prometheus( + telemetry, + ®istry, + config.role, + &config.network.node_name, + &config.impl_version, + )?; + spawn_handle.spawn( + "prometheus-endpoint", + None, + prometheus_endpoint::init_prometheus(port, registry).map(drop), + ); + + metrics + } else { + MetricsService::new(telemetry) + }; + + // Periodically updated metrics and telemetry updates. + spawn_handle.spawn( + "telemetry-periodic-send", + None, + metrics_service.run( + client.clone(), + transaction_pool.clone(), + network.clone(), + sync_service.clone(), + ), + ); + + let rpc_id_provider = config.rpc.id_provider.take(); + + // jsonrpsee RPC + let gen_rpc_module = || { + gen_rpc_module( + task_manager.spawn_handle(), + client.clone(), + transaction_pool.clone(), + keystore.clone(), + system_rpc_tx.clone(), + config.impl_name.clone(), + config.impl_version.clone(), + config.chain_spec.as_ref(), + &config.state_pruning, + config.blocks_pruning, + backend.clone(), + &*rpc_builder, + ) + }; + + let rpc_server_handle = start_rpc_servers( + &config.rpc, + config.prometheus_registry(), + &config.tokio_handle, + gen_rpc_module, + rpc_id_provider, + )?; + let in_memory_rpc = { + let mut module = gen_rpc_module()?; + module.extensions_mut().insert(DenyUnsafe::No); + module + }; + + let in_memory_rpc_handle = RpcHandlers::new(Arc::new(in_memory_rpc)); + + // Spawn informant task + spawn_handle.spawn( + "informant", + None, + sc_informant::build(client.clone(), network, sync_service.clone()), + ); + + task_manager.keep_alive((config.base_path, rpc_server_handle)); + + Ok(in_memory_rpc_handle) +} + +/// Returns a future that forwards imported transactions to the transaction networking protocol. +pub async fn propagate_transaction_notifications( + transaction_pool: Arc, + tx_handler_controller: sc_network_transactions::TransactionsHandlerController< + ::Hash, + >, + telemetry: Option, +) where + Block: BlockT, + ExPool: MaintainedTransactionPool::Hash>, +{ + // transaction notifications + transaction_pool + .import_notification_stream() + .for_each(move |hash| { + tx_handler_controller.propagate_transaction(hash); + let status = transaction_pool.status(); + telemetry!( + telemetry; + SUBSTRATE_INFO; + "txpool.import"; + "ready" => status.ready, + "future" => status.future, + ); + ready(()) + }) + .await; +} + +/// Initialize telemetry with provided configuration and return telemetry handle +pub fn init_telemetry( + name: String, + implementation: String, + version: String, + chain: String, + authority: bool, + network: Network, + client: Arc, + telemetry: &mut Telemetry, + sysinfo: Option, +) -> sc_telemetry::Result +where + Block: BlockT, + Client: BlockBackend, + Network: NetworkStateInfo, +{ + let genesis_hash = client + .block_hash(Zero::zero()) + .ok() + .flatten() + .unwrap_or_default(); + let connection_message = ConnectionMessage { + name, + implementation, + version, + target_os: sc_sysinfo::TARGET_OS.into(), + target_arch: sc_sysinfo::TARGET_ARCH.into(), + target_env: sc_sysinfo::TARGET_ENV.into(), + config: String::new(), + chain, + genesis_hash: format!("{:?}", genesis_hash), + authority, + startup_time: SystemTime::UNIX_EPOCH + .elapsed() + .map(|dur| dur.as_millis()) + .unwrap_or(0) + .to_string(), + network_id: network.local_peer_id().to_base58(), + sysinfo, + }; + + telemetry.start_telemetry(connection_message)?; + + Ok(telemetry.handle()) +} + +/// Generate RPC module using provided configuration +pub fn gen_rpc_module( + spawn_handle: SpawnTaskHandle, + client: Arc, + transaction_pool: Arc, + keystore: KeystorePtr, + system_rpc_tx: TracingUnboundedSender>, + impl_name: String, + impl_version: String, + chain_spec: &dyn ChainSpec, + state_pruning: &Option, + blocks_pruning: BlocksPruning, + backend: Arc, + rpc_builder: &dyn Fn(SubscriptionTaskExecutor) -> Result, Error>, +) -> Result, Error> +where + TBl: BlockT, + TCl: ProvideRuntimeApi + + BlockchainEvents + + HeaderBackend + + HeaderMetadata + + ExecutorProvider + + CallApiAt + + ProofProvider + + StorageProvider + + BlockBackend + + Send + + Sync + + 'static, + TBackend: sc_client_api::backend::Backend + 'static, + >::Api: sp_session::SessionKeys + sp_api::Metadata, + TExPool: MaintainedTransactionPool::Hash> + 'static, + TBl::Hash: Unpin, + TBl::Header: Unpin, +{ + let system_info = sc_rpc::system::SystemInfo { + chain_name: chain_spec.name().into(), + impl_name, + impl_version, + properties: chain_spec.properties(), + chain_type: chain_spec.chain_type(), + }; + + let mut rpc_api = RpcModule::new(()); + let task_executor = Arc::new(spawn_handle); + + let (chain, state, child_state) = { + let chain = sc_rpc::chain::new_full(client.clone(), task_executor.clone()).into_rpc(); + let (state, child_state) = sc_rpc::state::new_full(client.clone(), task_executor.clone()); + let state = state.into_rpc(); + let child_state = child_state.into_rpc(); + + (chain, state, child_state) + }; + + const MAX_TRANSACTION_PER_CONNECTION: usize = 16; + + let transaction_broadcast_rpc_v2 = sc_rpc_spec_v2::transaction::TransactionBroadcast::new( + client.clone(), + transaction_pool.clone(), + task_executor.clone(), + MAX_TRANSACTION_PER_CONNECTION, + ) + .into_rpc(); + + let transaction_v2 = sc_rpc_spec_v2::transaction::Transaction::new( + client.clone(), + transaction_pool.clone(), + task_executor.clone(), + ) + .into_rpc(); + + let chain_head_v2 = sc_rpc_spec_v2::chain_head::ChainHead::new( + client.clone(), + backend.clone(), + task_executor.clone(), + // Defaults to sensible limits for the `ChainHead`. + sc_rpc_spec_v2::chain_head::ChainHeadConfig::default(), + ) + .into_rpc(); + + // Part of the RPC v2 spec. + // An archive node that can respond to the `archive` RPC-v2 queries is a node with: + // - state pruning in archive mode: The storage of blocks is kept around + // - block pruning in archive mode: The block's body is kept around + let is_archive_node = state_pruning + .as_ref() + .map(|sp| sp.is_archive()) + .unwrap_or(false) + && blocks_pruning.is_archive(); + let genesis_hash = client + .hash(Zero::zero()) + .ok() + .flatten() + .expect("Genesis block exists; qed"); + if is_archive_node { + let archive_v2 = sc_rpc_spec_v2::archive::Archive::new( + client.clone(), + backend.clone(), + genesis_hash, + // Defaults to sensible limits for the `Archive`. + sc_rpc_spec_v2::archive::ArchiveConfig::default(), + ) + .into_rpc(); + rpc_api + .merge(archive_v2) + .map_err(|e| Error::Application(e.into()))?; + } + + // ChainSpec RPC-v2. + let chain_spec_v2 = sc_rpc_spec_v2::chain_spec::ChainSpec::new( + chain_spec.name().into(), + genesis_hash, + chain_spec.properties(), + ) + .into_rpc(); + + let author = sc_rpc::author::Author::new( + client.clone(), + transaction_pool, + keystore, + task_executor.clone(), + ) + .into_rpc(); + + let system = sc_rpc::system::System::new(system_info, system_rpc_tx).into_rpc(); + + if let Some(storage) = backend.offchain_storage() { + let offchain = sc_rpc::offchain::Offchain::new(storage).into_rpc(); + + rpc_api + .merge(offchain) + .map_err(|e| Error::Application(e.into()))?; + } + + // Part of the RPC v2 spec. + rpc_api + .merge(transaction_v2) + .map_err(|e| Error::Application(e.into()))?; + rpc_api + .merge(transaction_broadcast_rpc_v2) + .map_err(|e| Error::Application(e.into()))?; + rpc_api + .merge(chain_head_v2) + .map_err(|e| Error::Application(e.into()))?; + rpc_api + .merge(chain_spec_v2) + .map_err(|e| Error::Application(e.into()))?; + + // Part of the old RPC spec. + rpc_api + .merge(chain) + .map_err(|e| Error::Application(e.into()))?; + rpc_api + .merge(author) + .map_err(|e| Error::Application(e.into()))?; + rpc_api + .merge(system) + .map_err(|e| Error::Application(e.into()))?; + rpc_api + .merge(state) + .map_err(|e| Error::Application(e.into()))?; + rpc_api + .merge(child_state) + .map_err(|e| Error::Application(e.into()))?; + // Additional [`RpcModule`]s defined in the node to fit the specific blockchain + let extra_rpcs = rpc_builder(task_executor.clone())?; + rpc_api + .merge(extra_rpcs) + .map_err(|e| Error::Application(e.into()))?; + + Ok(rpc_api) +} + +/// Parameters to pass into `build_network`. +pub struct BuildNetworkParams< + 'a, + TBl: BlockT, + TNet: NetworkBackend::Hash>, + TExPool, + TImpQu, + TCl, +> { + /// The service configuration. + pub config: &'a Configuration, + /// Full network configuration. + pub net_config: FullNetworkConfiguration::Hash, TNet>, + /// A shared client returned by `new_full_parts`. + pub client: Arc, + /// A shared transaction pool. + pub transaction_pool: Arc, + /// A handle for spawning tasks. + pub spawn_handle: SpawnTaskHandle, + /// An import queue. + pub import_queue: TImpQu, + /// A block announce validator builder. + pub block_announce_validator_builder: + Option) -> Box + Send> + Send>>, + /// Optional warp sync config. + pub warp_sync_config: Option>, + /// User specified block relay params. If not specified, the default + /// block request handler will be used. + pub block_relay: Option>, + /// Metrics. + pub metrics: NotificationMetrics, +} + +/// Build the network service, the network status sinks and an RPC sender. +pub fn build_network( + params: BuildNetworkParams, +) -> Result< + ( + Arc, + TracingUnboundedSender>, + sc_network_transactions::TransactionsHandlerController<::Hash>, + NetworkStarter, + Arc>, + ), + Error, +> +where + TBl: BlockT, + TCl: ProvideRuntimeApi + + HeaderMetadata + + Chain + + BlockBackend + + BlockIdTo + + ProofProvider + + HeaderBackend + + BlockchainEvents + + 'static, + TExPool: TransactionPool::Hash> + 'static, + TImpQu: ImportQueue + 'static, + TNet: NetworkBackend::Hash>, +{ + let BuildNetworkParams { + config, + mut net_config, + client, + transaction_pool, + spawn_handle, + import_queue, + block_announce_validator_builder, + warp_sync_config, + block_relay, + metrics, + } = params; + + if warp_sync_config.is_none() && config.network.sync_mode.is_warp() { + return Err("Warp sync enabled, but no warp sync provider configured.".into()); + } + + if client.requires_full_sync() { + match config.network.sync_mode { + SyncMode::LightState { .. } => { + return Err("Fast sync doesn't work for archive nodes".into()) + } + SyncMode::Warp => return Err("Warp sync doesn't work for archive nodes".into()), + SyncMode::Full => {} + } + } + + let protocol_id = config.protocol_id(); + let genesis_hash = client + .block_hash(0u32.into()) + .ok() + .flatten() + .expect("Genesis block exists; qed"); + + let block_announce_validator = if let Some(f) = block_announce_validator_builder { + f(client.clone()) + } else { + Box::new(DefaultBlockAnnounceValidator) + }; + + let (chain_sync_network_provider, chain_sync_network_handle) = NetworkServiceProvider::new(); + let (mut block_server, block_downloader, block_request_protocol_config) = match block_relay { + Some(params) => ( + params.server, + params.downloader, + params.request_response_config, + ), + None => { + // Custom protocol was not specified, use the default block handler. + // Allow both outgoing and incoming requests. + let params = BlockRequestHandler::new::( + chain_sync_network_handle.clone(), + &protocol_id, + config.chain_spec.fork_id(), + client.clone(), + config.network.default_peers_set.in_peers as usize + + config.network.default_peers_set.out_peers as usize, + ); + ( + params.server, + params.downloader, + params.request_response_config, + ) + } + }; + spawn_handle.spawn("block-request-handler", Some("networking"), async move { + block_server.run().await; + }); + + let (state_request_protocol_config, state_request_protocol_name) = { + let num_peer_hint = net_config.network_config.default_peers_set_num_full as usize + + net_config + .network_config + .default_peers_set + .reserved_nodes + .len(); + // Allow both outgoing and incoming requests. + let (handler, protocol_config) = StateRequestHandler::new::( + &protocol_id, + config.chain_spec.fork_id(), + client.clone(), + num_peer_hint, + ); + let config_name = protocol_config.protocol_name().clone(); + + spawn_handle.spawn("state-request-handler", Some("networking"), handler.run()); + (protocol_config, config_name) + }; + + let (warp_sync_protocol_config, warp_request_protocol_name) = match warp_sync_config.as_ref() { + Some(WarpSyncConfig::WithProvider(warp_with_provider)) => { + // Allow both outgoing and incoming requests. + let (handler, protocol_config) = WarpSyncRequestHandler::new::<_, TNet>( + protocol_id.clone(), + genesis_hash, + config.chain_spec.fork_id(), + warp_with_provider.clone(), + ); + let config_name = protocol_config.protocol_name().clone(); + + spawn_handle.spawn( + "warp-sync-request-handler", + Some("networking"), + handler.run(), + ); + (Some(protocol_config), Some(config_name)) + } + _ => (None, None), + }; + + let light_client_request_protocol_config = { + // Allow both outgoing and incoming requests. + let (handler, protocol_config) = LightClientRequestHandler::new::( + &protocol_id, + config.chain_spec.fork_id(), + client.clone(), + ); + spawn_handle.spawn( + "light-client-request-handler", + Some("networking"), + handler.run(), + ); + protocol_config + }; + + // install request handlers to `FullNetworkConfiguration` + net_config.add_request_response_protocol(block_request_protocol_config); + net_config.add_request_response_protocol(state_request_protocol_config); + net_config.add_request_response_protocol(light_client_request_protocol_config); + + if let Some(config) = warp_sync_protocol_config { + net_config.add_request_response_protocol(config); + } + + let bitswap_config = config.network.ipfs_server.then(|| { + let (handler, config) = TNet::bitswap_server(client.clone()); + spawn_handle.spawn("bitswap-request-handler", Some("networking"), handler); + + config + }); + + // create transactions protocol and add it to the list of supported protocols of + let peer_store_handle = net_config.peer_store_handle(); + let (transactions_handler_proto, transactions_config) = + sc_network_transactions::TransactionsHandlerPrototype::new::<_, TBl, TNet>( + protocol_id.clone(), + genesis_hash, + config.chain_spec.fork_id(), + metrics.clone(), + Arc::clone(&peer_store_handle), + ); + net_config.add_notification_protocol(transactions_config); + + // Start task for `PeerStore` + let peer_store = net_config.take_peer_store(); + let peer_store_handle = peer_store.handle(); + spawn_handle.spawn("peer-store", Some("networking"), peer_store.run()); + + let (engine, sync_service, block_announce_config) = SyncingEngine::new( + Roles::from(&config.role), + client.clone(), + config + .prometheus_config + .as_ref() + .map(|config| config.registry.clone()) + .as_ref(), + metrics.clone(), + &net_config, + protocol_id.clone(), + &config.chain_spec.fork_id().map(ToOwned::to_owned), + block_announce_validator, + warp_sync_config, + chain_sync_network_handle, + import_queue.service(), + block_downloader, + state_request_protocol_name, + warp_request_protocol_name, + Arc::clone(&peer_store_handle), + )?; + let sync_service_import_queue = sync_service.clone(); + let sync_service = Arc::new(sync_service); + + let genesis_hash = client + .hash(Zero::zero()) + .ok() + .flatten() + .expect("Genesis block exists; qed"); + let network_params = sc_network::config::Params::::Hash, TNet> { + role: config.role, + executor: { + let spawn_handle = Clone::clone(&spawn_handle); + Box::new(move |fut| { + spawn_handle.spawn("libp2p-node", Some("networking"), fut); + }) + }, + network_config: net_config, + genesis_hash, + protocol_id: protocol_id.clone(), + fork_id: config.chain_spec.fork_id().map(ToOwned::to_owned), + metrics_registry: config + .prometheus_config + .as_ref() + .map(|config| config.registry.clone()), + block_announce_config, + bitswap_config, + notification_metrics: metrics, + }; + + let has_bootnodes = !network_params + .network_config + .network_config + .boot_nodes + .is_empty(); + let network_mut = TNet::new(network_params)?; + let network = network_mut.network_service().clone(); + + let (tx_handler, tx_handler_controller) = transactions_handler_proto.build( + network.clone(), + sync_service.clone(), + Arc::new(TransactionPoolAdapter { + pool: transaction_pool, + client: client.clone(), + }), + config + .prometheus_config + .as_ref() + .map(|config| &config.registry), + )?; + spawn_handle.spawn_blocking( + "network-transactions-handler", + Some("networking"), + tx_handler.run(), + ); + + spawn_handle.spawn_blocking( + "chain-sync-network-service-provider", + Some("networking"), + chain_sync_network_provider.run(Arc::new(network.clone())), + ); + spawn_handle.spawn( + "import-queue", + None, + import_queue.run(Box::new(sync_service_import_queue)), + ); + spawn_handle.spawn_blocking("syncing", None, engine.run()); + + let (system_rpc_tx, system_rpc_rx) = tracing_unbounded("mpsc_system_rpc", 10_000); + spawn_handle.spawn( + "system-rpc-handler", + Some("networking"), + build_system_rpc_future::<_, _, ::Hash>( + config.role, + network_mut.network_service(), + sync_service.clone(), + client.clone(), + system_rpc_rx, + has_bootnodes, + ), + ); + + let future = build_network_future::<_, _, ::Hash, _>( + network_mut, + client, + sync_service.clone(), + config.announce_block, + ); + + // TODO: Normally, one is supposed to pass a list of notifications protocols supported by the + // node through the `NetworkConfiguration` struct. But because this function doesn't know in + // advance which components, such as GrandPa or Polkadot, will be plugged on top of the + // service, it is unfortunately not possible to do so without some deep refactoring. To + // bypass this problem, the `NetworkService` provides a `register_notifications_protocol` + // method that can be called even after the network has been initialized. However, we want to + // avoid the situation where `register_notifications_protocol` is called *after* the network + // actually connects to other peers. For this reason, we delay the process of the network + // future until the user calls `NetworkStarter::start_network`. + // + // This entire hack should eventually be removed in favour of passing the list of protocols + // through the configuration. + // + // See also https://github.com/paritytech/substrate/issues/6827 + let (network_start_tx, network_start_rx) = oneshot::channel(); + + // The network worker is responsible for gathering all network messages and processing + // them. This is quite a heavy task, and at the time of the writing of this comment it + // frequently happens that this future takes several seconds or in some situations + // even more than a minute until it has processed its entire queue. This is clearly an + // issue, and ideally we would like to fix the network future to take as little time as + // possible, but we also take the extra harm-prevention measure to execute the networking + // future using `spawn_blocking`. + spawn_handle.spawn_blocking("network-worker", Some("networking"), async move { + if network_start_rx.await.is_err() { + log::warn!( + "The NetworkStart returned as part of `build_network` has been silently dropped" + ); + // This `return` might seem unnecessary, but we don't want to make it look like + // everything is working as normal even though the user is clearly misusing the API. + return; + } + + future.await + }); + + Ok(( + network, + system_rpc_tx, + tx_handler_controller, + NetworkStarter(network_start_tx), + sync_service.clone(), + )) +} + +/// Object used to start the network. +#[must_use] +pub struct NetworkStarter(oneshot::Sender<()>); + +impl NetworkStarter { + /// Create a new NetworkStarter + pub fn new(sender: oneshot::Sender<()>) -> Self { + NetworkStarter(sender) + } + + /// Start the network. Call this after all sub-components have been initialized. + /// + /// > **Note**: If you don't call this function, the networking will not work. + pub fn start_network(self) { + let _ = self.0.send(()); + } +} diff --git a/substrate/service/src/chain_ops/check_block.rs b/substrate/service/src/chain_ops/check_block.rs new file mode 100644 index 00000000000..aab65de8827 --- /dev/null +++ b/substrate/service/src/chain_ops/check_block.rs @@ -0,0 +1,39 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::error::Error; +use codec::Encode; +use sc_client_api::{BlockBackend, HeaderBackend}; +use sc_consensus::import_queue::ImportQueue; +use sp_runtime::{generic::BlockId, traits::Block as BlockT}; + +use crate::chain_ops::import_blocks; +use std::sync::Arc; + +/// Re-validate known block. +pub async fn check_block( + client: Arc, + import_queue: IQ, + block_id: BlockId, +) -> Result<(), Error> +where + C: BlockBackend + HeaderBackend + Send + Sync + 'static, + B: BlockT + for<'de> serde::Deserialize<'de>, + IQ: ImportQueue + 'static, +{ + let maybe_block = client + .block_hash_from_id(&block_id)? + .map(|hash| client.block(hash)) + .transpose()? + .flatten(); + match maybe_block { + Some(block) => { + let mut buf = Vec::new(); + 1u64.encode_to(&mut buf); + block.encode_to(&mut buf); + let reader = std::io::Cursor::new(buf); + import_blocks(client, import_queue, reader, true, true).await + } + None => Err("Unknown block")?, + } +} diff --git a/substrate/service/src/chain_ops/export_blocks.rs b/substrate/service/src/chain_ops/export_blocks.rs new file mode 100644 index 00000000000..3e79e660f30 --- /dev/null +++ b/substrate/service/src/chain_ops/export_blocks.rs @@ -0,0 +1,93 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::error::Error; +use codec::Encode; +use futures::{future, prelude::*}; +use log::info; +use sp_runtime::{ + generic::BlockId, + traits::{Block as BlockT, NumberFor, One, SaturatedConversion, Zero}, +}; + +use sc_client_api::{BlockBackend, HeaderBackend, UsageProvider}; +use std::{io::Write, pin::Pin, sync::Arc, task::Poll}; + +/// Performs the blocks export. +pub fn export_blocks( + client: Arc, + mut output: impl Write + 'static, + from: NumberFor, + to: Option>, + binary: bool, +) -> Pin>>> +where + C: HeaderBackend + BlockBackend + UsageProvider + 'static, + B: BlockT, +{ + let mut block = from; + + let last = match to { + Some(v) if v.is_zero() => One::one(), + Some(v) => v, + None => client.usage_info().chain.best_number, + }; + + let mut wrote_header = false; + + // Exporting blocks is implemented as a future, because we want the operation to be + // interruptible. + // + // Every time we write a block to the output, the `Future` re-schedules itself and returns + // `Poll::Pending`. + // This makes it possible either to interleave other operations in-between the block exports, + // or to stop the operation completely. + let export = future::poll_fn(move |cx| { + let client = &client; + + if last < block { + return Poll::Ready(Err("Invalid block range specified".into())); + } + + if !wrote_header { + info!("Exporting blocks from #{} to #{}", block, last); + if binary { + let last_: u64 = last.saturated_into::(); + let block_: u64 = block.saturated_into::(); + let len: u64 = last_ - block_ + 1; + output.write_all(&len.encode())?; + } + wrote_header = true; + } + + match client + .block_hash_from_id(&BlockId::number(block))? + .map(|hash| client.block(hash)) + .transpose()? + .flatten() + { + Some(block) => { + if binary { + output.write_all(&block.encode())?; + } else { + serde_json::to_writer(&mut output, &block) + .map_err(|e| format!("Error writing JSON: {}", e))?; + } + } + None => return Poll::Ready(Ok(())), + } + if (block % 10000u32.into()).is_zero() { + info!("#{}", block); + } + if block == last { + return Poll::Ready(Ok(())); + } + block += One::one(); + + // Re-schedule the task in order to continue the operation. + cx.waker().wake_by_ref(); + Poll::Pending + }); + + Box::pin(export) +} diff --git a/substrate/service/src/chain_ops/export_raw_state.rs b/substrate/service/src/chain_ops/export_raw_state.rs new file mode 100644 index 00000000000..a41106890a8 --- /dev/null +++ b/substrate/service/src/chain_ops/export_raw_state.rs @@ -0,0 +1,60 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::error::Error; +use sc_client_api::{StorageProvider, UsageProvider}; +use sp_core::storage::{well_known_keys, ChildInfo, Storage, StorageChild, StorageKey, StorageMap}; +use sp_runtime::traits::Block as BlockT; + +use std::{ + collections::{BTreeMap, HashMap}, + sync::Arc, +}; + +/// Export the raw state at the given `block`. If `block` is `None`, the +/// best block will be used. +pub fn export_raw_state(client: Arc, hash: B::Hash) -> Result +where + C: UsageProvider + StorageProvider, + B: BlockT, + BA: sc_client_api::backend::Backend, +{ + let mut top = BTreeMap::new(); + let mut children_default = HashMap::new(); + + for (key, value) in client.storage_pairs(hash, None, None)? { + // Remove all default child storage roots from the top storage and collect the child storage + // pairs. + if key + .0 + .starts_with(well_known_keys::DEFAULT_CHILD_STORAGE_KEY_PREFIX) + { + let child_root_key = StorageKey( + key.0[well_known_keys::DEFAULT_CHILD_STORAGE_KEY_PREFIX.len()..].to_vec(), + ); + let child_info = ChildInfo::new_default(&child_root_key.0); + let mut pairs = StorageMap::new(); + for child_key in client.child_storage_keys(hash, child_info.clone(), None, None)? { + if let Some(child_value) = client.child_storage(hash, &child_info, &child_key)? { + pairs.insert(child_key.0, child_value.0); + } + } + + children_default.insert( + child_root_key.0, + StorageChild { + child_info, + data: pairs, + }, + ); + continue; + } + + top.insert(key.0, value.0); + } + + Ok(Storage { + top, + children_default, + }) +} diff --git a/substrate/service/src/chain_ops/import_blocks.rs b/substrate/service/src/chain_ops/import_blocks.rs new file mode 100644 index 00000000000..63eff15a58a --- /dev/null +++ b/substrate/service/src/chain_ops/import_blocks.rs @@ -0,0 +1,512 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::{error, error::Error}; +use codec::{Decode, IoReader as CodecIoReader}; +use futures::{future, prelude::*}; +use futures_timer::Delay; +use log::{info, warn}; +use sc_chain_spec::ChainSpec; +use sc_client_api::HeaderBackend; +use sc_consensus::import_queue::{ + BlockImportError, BlockImportStatus, ImportQueue, IncomingBlock, Link, +}; +use serde_json::{de::IoRead as JsonIoRead, Deserializer, StreamDeserializer}; +use sp_consensus::BlockOrigin; +use sp_runtime::{ + generic::SignedBlock, + traits::{ + Block as BlockT, CheckedDiv, Header, MaybeSerializeDeserialize, NumberFor, Saturating, Zero, + }, +}; +use std::{ + io::Read, + pin::Pin, + task::Poll, + time::{Duration, Instant}, +}; + +/// Number of blocks we will add to the queue before waiting for the queue to catch up. +const MAX_PENDING_BLOCKS: u64 = 10_000; + +/// Number of milliseconds to wait until next poll. +const DELAY_TIME: u64 = 200; + +/// Number of milliseconds that must have passed between two updates. +const TIME_BETWEEN_UPDATES: u64 = 3_000; + +use std::sync::Arc; + +/// Build a chain spec json +pub fn build_spec(spec: &dyn ChainSpec, raw: bool) -> error::Result { + spec.as_json(raw).map_err(Into::into) +} + +/// Helper enum that wraps either a binary decoder (from parity-scale-codec), or a JSON decoder +/// (from serde_json). Implements the Iterator Trait, calling `next()` will decode the next +/// SignedBlock and return it. +enum BlockIter +where + R: std::io::Read, +{ + Binary { + // Total number of blocks we are expecting to decode. + num_expected_blocks: u64, + // Number of blocks we have decoded thus far. + read_block_count: u64, + // Reader to the data, used for decoding new blocks. + reader: CodecIoReader, + }, + Json { + // Number of blocks we have decoded thus far. + read_block_count: u64, + // Stream to the data, used for decoding new blocks. + reader: StreamDeserializer<'static, JsonIoRead, SignedBlock>, + }, +} + +impl BlockIter +where + R: Read + 'static, + B: BlockT + MaybeSerializeDeserialize, +{ + fn new(input: R, binary: bool) -> Result { + if binary { + let mut reader = CodecIoReader(input); + // If the file is encoded in binary format, it is expected to first specify the number + // of blocks that are going to be decoded. We read it and add it to our enum struct. + let num_expected_blocks: u64 = Decode::decode(&mut reader) + .map_err(|e| format!("Failed to decode the number of blocks: {:?}", e))?; + Ok(BlockIter::Binary { + num_expected_blocks, + read_block_count: 0, + reader, + }) + } else { + let stream_deser = Deserializer::from_reader(input).into_iter::>(); + Ok(BlockIter::Json { + reader: stream_deser, + read_block_count: 0, + }) + } + } + + /// Returns the number of blocks read thus far. + fn read_block_count(&self) -> u64 { + match self { + BlockIter::Binary { + read_block_count, .. + } + | BlockIter::Json { + read_block_count, .. + } => *read_block_count, + } + } + + /// Returns the total number of blocks to be imported, if possible. + fn num_expected_blocks(&self) -> Option { + match self { + BlockIter::Binary { + num_expected_blocks, + .. + } => Some(*num_expected_blocks), + BlockIter::Json { .. } => None, + } + } +} + +impl Iterator for BlockIter +where + R: Read + 'static, + B: BlockT + MaybeSerializeDeserialize, +{ + type Item = Result, String>; + + fn next(&mut self) -> Option { + match self { + BlockIter::Binary { + num_expected_blocks, + read_block_count, + reader, + } => { + if read_block_count < num_expected_blocks { + let block_result: Result, _> = + SignedBlock::::decode(reader).map_err(|e| e.to_string()); + *read_block_count += 1; + Some(block_result) + } else { + // `read_block_count` == `num_expected_blocks` so we've read enough blocks. + None + } + } + BlockIter::Json { + reader, + read_block_count, + } => { + let res = Some(reader.next()?.map_err(|e| e.to_string())); + *read_block_count += 1; + res + } + } + } +} + +/// Imports the SignedBlock to the queue. +fn import_block_to_queue( + signed_block: SignedBlock, + queue: &mut TImpQu, + force: bool, +) where + TBl: BlockT + MaybeSerializeDeserialize, + TImpQu: 'static + ImportQueue, +{ + let (header, extrinsics) = signed_block.block.deconstruct(); + let hash = header.hash(); + // import queue handles verification and importing it into the client. + queue.service_ref().import_blocks( + BlockOrigin::File, + vec![IncomingBlock:: { + hash, + header: Some(header), + body: Some(extrinsics), + indexed_body: None, + justifications: signed_block.justifications, + origin: None, + allow_missing_state: false, + import_existing: force, + state: None, + skip_execution: false, + }], + ); +} + +/// Returns true if we have imported every block we were supposed to import, else returns false. +fn importing_is_done( + num_expected_blocks: Option, + read_block_count: u64, + imported_blocks: u64, +) -> bool { + if let Some(num_expected_blocks) = num_expected_blocks { + imported_blocks >= num_expected_blocks + } else { + imported_blocks >= read_block_count + } +} + +/// Structure used to log the block importing speed. +struct Speedometer { + best_number: NumberFor, + last_number: Option>, + last_update: Instant, +} + +impl Speedometer { + /// Creates a fresh Speedometer. + fn new() -> Self { + Self { + best_number: NumberFor::::from(0u32), + last_number: None, + last_update: Instant::now(), + } + } + + /// Calculates `(best_number - last_number) / (now - last_update)` and + /// logs the speed of import. + fn display_speed(&self) { + // Number of milliseconds elapsed since last time. + let elapsed_ms = { + let elapsed = self.last_update.elapsed(); + let since_last_millis = elapsed.as_secs() * 1000; + let since_last_subsec_millis = elapsed.subsec_millis() as u64; + since_last_millis + since_last_subsec_millis + }; + + // Number of blocks that have been imported since last time. + let diff = match self.last_number { + None => return, + Some(n) => self.best_number.saturating_sub(n), + }; + + if let Ok(diff) = TryInto::::try_into(diff) { + // If the number of blocks can be converted to a regular integer, then it's easy: just + // do the math and turn it into a `f64`. + let speed = diff + .saturating_mul(10_000) + .checked_div(u128::from(elapsed_ms)) + .map_or(0.0, |s| s as f64) + / 10.0; + info!( + "📦 Current best block: {} ({:4.1} bps)", + self.best_number, speed + ); + } else { + // If the number of blocks can't be converted to a regular integer, then we need a more + // algebraic approach and we stay within the realm of integers. + let one_thousand = NumberFor::::from(1_000u32); + let elapsed = + NumberFor::::from(>::try_from(elapsed_ms).unwrap_or(u32::MAX)); + + let speed = diff + .saturating_mul(one_thousand) + .checked_div(&elapsed) + .unwrap_or_else(Zero::zero); + info!( + "📦 Current best block: {} ({} bps)", + self.best_number, speed + ) + } + } + + /// Updates the Speedometer. + fn update(&mut self, best_number: NumberFor) { + self.last_number = Some(self.best_number); + self.best_number = best_number; + self.last_update = Instant::now(); + } + + // If more than TIME_BETWEEN_UPDATES has elapsed since last update, + // then print and update the speedometer. + fn notify_user(&mut self, best_number: NumberFor) { + let delta = Duration::from_millis(TIME_BETWEEN_UPDATES); + if Instant::now().duration_since(self.last_update) >= delta { + self.display_speed(); + self.update(best_number); + } + } +} + +/// Different State that the `import_blocks` future could be in. +enum ImportState +where + R: Read + 'static, + B: BlockT + MaybeSerializeDeserialize, +{ + /// We are reading from the [`BlockIter`] structure, adding those blocks to the queue if + /// possible. + Reading { block_iter: BlockIter }, + /// The queue is full (contains at least MAX_PENDING_BLOCKS blocks) and we are waiting for it + /// to catch up. + WaitingForImportQueueToCatchUp { + block_iter: BlockIter, + delay: Delay, + block: SignedBlock, + }, + // We have added all the blocks to the queue but they are still being processed. + WaitingForImportQueueToFinish { + num_expected_blocks: Option, + read_block_count: u64, + delay: Delay, + }, +} + +/// Starts the process of importing blocks. +pub fn import_blocks( + client: Arc, + mut import_queue: IQ, + input: impl Read + Send + 'static, + force: bool, + binary: bool, +) -> Pin> + Send>> +where + C: HeaderBackend + Send + Sync + 'static, + B: BlockT + for<'de> serde::Deserialize<'de>, + IQ: ImportQueue + 'static, +{ + struct WaitLink { + imported_blocks: u64, + has_error: bool, + } + + impl WaitLink { + fn new() -> WaitLink { + WaitLink { + imported_blocks: 0, + has_error: false, + } + } + } + + impl Link for WaitLink { + fn blocks_processed( + &mut self, + imported: usize, + _num_expected_blocks: usize, + results: Vec<( + Result>, BlockImportError>, + B::Hash, + )>, + ) { + self.imported_blocks += imported as u64; + + for result in results { + if let (Err(err), hash) = result { + warn!( + "There was an error importing block with hash {:?}: {}", + hash, err + ); + self.has_error = true; + break; + } + } + } + } + + let mut link = WaitLink::new(); + let block_iter_res: Result, String> = BlockIter::new(input, binary); + + let block_iter = match block_iter_res { + Ok(block_iter) => block_iter, + Err(e) => { + // We've encountered an error while creating the block iterator + // so we can just return a future that returns an error. + return future::ready(Err(Error::Other(e))).boxed(); + } + }; + + let mut state = Some(ImportState::Reading { block_iter }); + let mut speedometer = Speedometer::::new(); + + // Importing blocks is implemented as a future, because we want the operation to be + // interruptible. + // + // Every time we read a block from the input or import a bunch of blocks from the import + // queue, the `Future` re-schedules itself and returns `Poll::Pending`. + // This makes it possible either to interleave other operations in-between the block imports, + // or to stop the operation completely. + let import = future::poll_fn(move |cx| { + let client = &client; + let queue = &mut import_queue; + match state.take().expect("state should never be None; qed") { + ImportState::Reading { mut block_iter } => { + match block_iter.next() { + None => { + // The iterator is over: we now need to wait for the import queue to finish. + let num_expected_blocks = block_iter.num_expected_blocks(); + let read_block_count = block_iter.read_block_count(); + let delay = Delay::new(Duration::from_millis(DELAY_TIME)); + state = Some(ImportState::WaitingForImportQueueToFinish { + num_expected_blocks, + read_block_count, + delay, + }); + } + Some(block_result) => { + let read_block_count = block_iter.read_block_count(); + match block_result { + Ok(block) => { + if read_block_count - link.imported_blocks >= MAX_PENDING_BLOCKS { + // The queue is full, so do not add this block and simply wait + // until the queue has made some progress. + let delay = Delay::new(Duration::from_millis(DELAY_TIME)); + state = Some(ImportState::WaitingForImportQueueToCatchUp { + block_iter, + delay, + block, + }); + } else { + // Queue is not full, we can keep on adding blocks to the queue. + import_block_to_queue(block, queue, force); + state = Some(ImportState::Reading { block_iter }); + } + } + Err(e) => { + return Poll::Ready(Err(Error::Other(format!( + "Error reading block #{}: {}", + read_block_count, e + )))) + } + } + } + } + } + ImportState::WaitingForImportQueueToCatchUp { + block_iter, + mut delay, + block, + } => { + let read_block_count = block_iter.read_block_count(); + if read_block_count - link.imported_blocks >= MAX_PENDING_BLOCKS { + // Queue is still full, so wait until there is room to insert our block. + match Pin::new(&mut delay).poll(cx) { + Poll::Pending => { + state = Some(ImportState::WaitingForImportQueueToCatchUp { + block_iter, + delay, + block, + }); + return Poll::Pending; + } + Poll::Ready(_) => { + delay.reset(Duration::from_millis(DELAY_TIME)); + } + } + state = Some(ImportState::WaitingForImportQueueToCatchUp { + block_iter, + delay, + block, + }); + } else { + // Queue is no longer full, so we can add our block to the queue. + import_block_to_queue(block, queue, force); + // Switch back to Reading state. + state = Some(ImportState::Reading { block_iter }); + } + } + ImportState::WaitingForImportQueueToFinish { + num_expected_blocks, + read_block_count, + mut delay, + } => { + // All the blocks have been added to the queue, which doesn't mean they + // have all been properly imported. + if importing_is_done(num_expected_blocks, read_block_count, link.imported_blocks) { + // Importing is done, we can log the result and return. + info!( + "🎉 Imported {} blocks. Best: #{}", + read_block_count, + client.info().best_number + ); + return Poll::Ready(Ok(())); + } else { + // Importing is not done, we still have to wait for the queue to finish. + // Wait for the delay, because we know the queue is lagging behind. + match Pin::new(&mut delay).poll(cx) { + Poll::Pending => { + state = Some(ImportState::WaitingForImportQueueToFinish { + num_expected_blocks, + read_block_count, + delay, + }); + return Poll::Pending; + } + Poll::Ready(_) => { + delay.reset(Duration::from_millis(DELAY_TIME)); + } + } + + state = Some(ImportState::WaitingForImportQueueToFinish { + num_expected_blocks, + read_block_count, + delay, + }); + } + } + } + + queue.poll_actions(cx, &mut link); + + let best_number = client.info().best_number; + speedometer.notify_user(best_number); + + if link.has_error { + return Poll::Ready(Err(Error::Other(format!( + "Stopping after #{} blocks because of an error", + link.imported_blocks + )))); + } + + cx.waker().wake_by_ref(); + Poll::Pending + }); + Box::pin(import) +} diff --git a/substrate/service/src/chain_ops/mod.rs b/substrate/service/src/chain_ops/mod.rs new file mode 100644 index 00000000000..8eb3053e4d7 --- /dev/null +++ b/substrate/service/src/chain_ops/mod.rs @@ -0,0 +1,16 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Chain utilities. + +mod check_block; +mod export_blocks; +mod export_raw_state; +mod import_blocks; +mod revert_chain; + +pub use check_block::*; +pub use export_blocks::*; +pub use export_raw_state::*; +pub use import_blocks::*; +pub use revert_chain::*; diff --git a/substrate/service/src/chain_ops/revert_chain.rs b/substrate/service/src/chain_ops/revert_chain.rs new file mode 100644 index 00000000000..e74077db9af --- /dev/null +++ b/substrate/service/src/chain_ops/revert_chain.rs @@ -0,0 +1,40 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::error::Error; +use log::info; +use sc_client_api::{Backend, UsageProvider}; +use sp_runtime::traits::{Block as BlockT, NumberFor, Zero}; +use std::sync::Arc; + +/// Performs a revert of `blocks` blocks. +pub fn revert_chain( + client: Arc, + backend: Arc, + blocks: NumberFor, +) -> Result<(), Error> +where + B: BlockT, + C: UsageProvider, + BA: Backend, +{ + let reverted = backend.revert(blocks, false)?; + let info = client.usage_info().chain; + + if reverted.0.is_zero() { + info!("There aren't any non-finalized blocks to revert."); + } else { + info!( + "Reverted {} blocks. Best: #{} ({})", + reverted.0, info.best_number, info.best_hash + ); + + if reverted.0 > blocks { + info!( + "Number of reverted blocks is higher than requested \ + because of reverted leaves higher than the best block." + ) + } + } + Ok(()) +} diff --git a/substrate/service/src/client/block_rules.rs b/substrate/service/src/client/block_rules.rs new file mode 100644 index 00000000000..32db963407a --- /dev/null +++ b/substrate/service/src/client/block_rules.rs @@ -0,0 +1,59 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Client fixed chain specification rules + +use std::collections::{HashMap, HashSet}; + +use sp_runtime::traits::{Block as BlockT, NumberFor}; + +use sc_client_api::{BadBlocks, ForkBlocks}; + +/// Chain specification rules lookup result. +pub enum LookupResult { + /// Specification rules do not contain any special rules about this block + NotSpecial, + /// The block is known to be bad and should not be imported + KnownBad, + /// There is a specified canonical block hash for the given height + Expected(B::Hash), +} + +/// Chain-specific block filtering rules. +/// +/// This holds known bad blocks and known good forks, and +/// is usually part of the chain spec. +pub struct BlockRules { + bad: HashSet, + forks: HashMap, B::Hash>, +} + +impl BlockRules { + /// New block rules with provided black and white lists. + pub fn new(fork_blocks: ForkBlocks, bad_blocks: BadBlocks) -> Self { + Self { + bad: bad_blocks.unwrap_or_default(), + forks: fork_blocks.unwrap_or_default().into_iter().collect(), + } + } + + /// Mark a new block as bad. + pub fn mark_bad(&mut self, hash: B::Hash) { + self.bad.insert(hash); + } + + /// Check if there's any rule affecting the given block. + pub fn lookup(&self, number: NumberFor, hash: &B::Hash) -> LookupResult { + if let Some(hash_for_height) = self.forks.get(&number) { + if hash_for_height != hash { + return LookupResult::Expected(*hash_for_height); + } + } + + if self.bad.contains(hash) { + return LookupResult::KnownBad; + } + + LookupResult::NotSpecial + } +} diff --git a/substrate/service/src/client/call_executor.rs b/substrate/service/src/client/call_executor.rs new file mode 100644 index 00000000000..f440fa73727 --- /dev/null +++ b/substrate/service/src/client/call_executor.rs @@ -0,0 +1,269 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use super::{code_provider::CodeProvider, ClientConfig}; +use sc_client_api::{ + backend, call_executor::CallExecutor, execution_extensions::ExecutionExtensions, HeaderBackend, +}; +use sc_executor::{RuntimeVersion, RuntimeVersionOf}; +use sp_api::ProofRecorder; +use sp_core::traits::{CallContext, CodeExecutor}; +use sp_externalities::Extensions; +use sp_runtime::{ + generic::BlockId, + traits::{Block as BlockT, HashingFor}, +}; +use sp_state_machine::{backend::AsTrieBackend, OverlayedChanges, StateMachine, StorageProof}; +use std::{cell::RefCell, sync::Arc}; + +/// Call executor that executes methods locally, querying all required +/// data from local backend. +pub struct LocalCallExecutor { + backend: Arc, + executor: E, + code_provider: CodeProvider, + execution_extensions: Arc>, +} + +impl LocalCallExecutor +where + E: CodeExecutor + RuntimeVersionOf + Clone + 'static, + B: backend::Backend, +{ + /// Creates new instance of local call executor. + pub fn new( + backend: Arc, + executor: E, + client_config: ClientConfig, + execution_extensions: ExecutionExtensions, + ) -> sp_blockchain::Result { + let code_provider = CodeProvider::new(&client_config, executor.clone(), backend.clone())?; + + Ok(LocalCallExecutor { + backend, + executor, + code_provider, + execution_extensions: Arc::new(execution_extensions), + }) + } +} + +impl Clone for LocalCallExecutor +where + E: Clone, +{ + fn clone(&self) -> Self { + LocalCallExecutor { + backend: self.backend.clone(), + executor: self.executor.clone(), + code_provider: self.code_provider.clone(), + execution_extensions: self.execution_extensions.clone(), + } + } +} + +impl CallExecutor for LocalCallExecutor +where + B: backend::Backend, + E: CodeExecutor + RuntimeVersionOf + Clone + 'static, + Block: BlockT, +{ + type Error = E::Error; + + type Backend = B; + + fn execution_extensions(&self) -> &ExecutionExtensions { + &self.execution_extensions + } + + fn call( + &self, + at_hash: Block::Hash, + method: &str, + call_data: &[u8], + context: CallContext, + ) -> sp_blockchain::Result> { + let mut changes = OverlayedChanges::default(); + let at_number = self + .backend + .blockchain() + .expect_block_number_from_id(&BlockId::Hash(at_hash))?; + let state = self.backend.state_at(at_hash)?; + + let state_runtime_code = sp_state_machine::backend::BackendRuntimeCode::new(&state); + let runtime_code = state_runtime_code + .runtime_code() + .map_err(sp_blockchain::Error::RuntimeCode)?; + + let runtime_code = self + .code_provider + .maybe_override_code(runtime_code, &state, at_hash)? + .0; + + let mut extensions = self.execution_extensions.extensions(at_hash, at_number); + + let mut sm = StateMachine::new( + &state, + &mut changes, + &self.executor, + method, + call_data, + &mut extensions, + &runtime_code, + context, + ) + .set_parent_hash(at_hash); + + sm.execute().map_err(Into::into) + } + + fn contextual_call( + &self, + at_hash: Block::Hash, + method: &str, + call_data: &[u8], + changes: &RefCell>>, + recorder: &Option>, + call_context: CallContext, + extensions: &RefCell, + ) -> Result, sp_blockchain::Error> { + let state = self.backend.state_at(at_hash)?; + + let changes = &mut *changes.borrow_mut(); + + // It is important to extract the runtime code here before we create the proof + // recorder to not record it. We also need to fetch the runtime code from `state` to + // make sure we use the caching layers. + let state_runtime_code = sp_state_machine::backend::BackendRuntimeCode::new(&state); + + let runtime_code = state_runtime_code + .runtime_code() + .map_err(sp_blockchain::Error::RuntimeCode)?; + let runtime_code = self + .code_provider + .maybe_override_code(runtime_code, &state, at_hash)? + .0; + let mut extensions = extensions.borrow_mut(); + + match recorder { + Some(recorder) => { + let trie_state = state.as_trie_backend(); + + let backend = sp_state_machine::TrieBackendBuilder::wrap(trie_state) + .with_recorder(recorder.clone()) + .build(); + + let mut state_machine = StateMachine::new( + &backend, + changes, + &self.executor, + method, + call_data, + &mut extensions, + &runtime_code, + call_context, + ) + .set_parent_hash(at_hash); + state_machine.execute() + } + None => { + let mut state_machine = StateMachine::new( + &state, + changes, + &self.executor, + method, + call_data, + &mut extensions, + &runtime_code, + call_context, + ) + .set_parent_hash(at_hash); + state_machine.execute() + } + } + .map_err(Into::into) + } + + fn runtime_version(&self, at_hash: Block::Hash) -> sp_blockchain::Result { + let state = self.backend.state_at(at_hash)?; + let state_runtime_code = sp_state_machine::backend::BackendRuntimeCode::new(&state); + + let runtime_code = state_runtime_code + .runtime_code() + .map_err(sp_blockchain::Error::RuntimeCode)?; + self.code_provider + .maybe_override_code(runtime_code, &state, at_hash) + .map(|(_, v)| v) + } + + fn prove_execution( + &self, + at_hash: Block::Hash, + method: &str, + call_data: &[u8], + ) -> sp_blockchain::Result<(Vec, StorageProof)> { + let at_number = self + .backend + .blockchain() + .expect_block_number_from_id(&BlockId::Hash(at_hash))?; + let state = self.backend.state_at(at_hash)?; + + let trie_backend = state.as_trie_backend(); + + let state_runtime_code = sp_state_machine::backend::BackendRuntimeCode::new(trie_backend); + let runtime_code = state_runtime_code + .runtime_code() + .map_err(sp_blockchain::Error::RuntimeCode)?; + let runtime_code = self + .code_provider + .maybe_override_code(runtime_code, &state, at_hash)? + .0; + + sp_state_machine::prove_execution_on_trie_backend( + trie_backend, + &mut Default::default(), + &self.executor, + method, + call_data, + &runtime_code, + &mut self.execution_extensions.extensions(at_hash, at_number), + ) + .map_err(Into::into) + } +} + +impl RuntimeVersionOf for LocalCallExecutor +where + E: RuntimeVersionOf, + Block: BlockT, +{ + fn runtime_version( + &self, + ext: &mut dyn sp_externalities::Externalities, + runtime_code: &sp_core::traits::RuntimeCode, + ) -> Result { + RuntimeVersionOf::runtime_version(&self.executor, ext, runtime_code) + } +} + +impl sp_version::GetRuntimeVersionAt for LocalCallExecutor +where + B: backend::Backend, + E: CodeExecutor + RuntimeVersionOf + Clone + 'static, + Block: BlockT, +{ + fn runtime_version(&self, at: Block::Hash) -> Result { + CallExecutor::runtime_version(self, at).map_err(|e| e.to_string()) + } +} + +impl sp_version::GetNativeVersion for LocalCallExecutor +where + B: backend::Backend, + E: CodeExecutor + sp_version::GetNativeVersion + Clone + 'static, + Block: BlockT, +{ + fn native_version(&self) -> &sp_version::NativeVersion { + self.executor.native_version() + } +} diff --git a/substrate/service/src/client/client.rs b/substrate/service/src/client/client.rs new file mode 100644 index 00000000000..636c3297948 --- /dev/null +++ b/substrate/service/src/client/client.rs @@ -0,0 +1,2188 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Substrate Client + +use super::{ + block_rules::{BlockRules, LookupResult as BlockLookupResult}, + CodeProvider, +}; +use crate::client::notification_pinning::NotificationPinningWorker; +use log::{debug, info, trace, warn}; +use parking_lot::{Mutex, RwLock}; +use prometheus_endpoint::Registry; +use rand::Rng; +use sc_chain_spec::{resolve_state_version_from_wasm, BuildGenesisBlock}; +use sc_client_api::{ + backend::{ + self, apply_aux, BlockImportOperation, ClientImportOperation, FinalizeSummary, Finalizer, + ImportNotificationAction, ImportSummary, LockImportRun, NewBlockState, StorageProvider, + }, + client::{ + BadBlocks, BlockBackend, BlockImportNotification, BlockOf, BlockchainEvents, ClientInfo, + FinalityNotification, FinalityNotifications, ForkBlocks, ImportNotifications, + PreCommitActions, ProvideUncles, + }, + execution_extensions::ExecutionExtensions, + notifications::{StorageEventStream, StorageNotifications}, + CallExecutor, ExecutorProvider, KeysIter, OnFinalityAction, OnImportAction, PairsIter, + ProofProvider, UnpinWorkerMessage, UsageProvider, +}; +use sc_consensus::{ + BlockCheckParams, BlockImportParams, ForkChoiceStrategy, ImportResult, StateAction, +}; +use sc_executor::RuntimeVersion; +use sc_telemetry::{telemetry, TelemetryHandle, SUBSTRATE_INFO}; +use sp_api::{ + ApiExt, ApiRef, CallApiAt, CallApiAtParams, ConstructRuntimeApi, Core as CoreApi, + ProvideRuntimeApi, +}; +use sp_blockchain::{ + self as blockchain, Backend as ChainBackend, CachedHeaderMetadata, Error, + HeaderBackend as ChainHeaderBackend, HeaderMetadata, Info as BlockchainInfo, +}; +use sp_consensus::{BlockOrigin, BlockStatus, Error as ConsensusError}; + +use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedSender}; +use sp_core::{ + storage::{ChildInfo, ChildType, PrefixedStorageKey, StorageChild, StorageData, StorageKey}, + traits::{CallContext, SpawnNamed}, +}; +use sp_runtime::{ + generic::{BlockId, SignedBlock}, + traits::{ + Block as BlockT, BlockIdTo, HashingFor, Header as HeaderT, NumberFor, One, + SaturatedConversion, Zero, + }, + Justification, Justifications, StateVersion, +}; +use sp_state_machine::{ + prove_child_read, prove_range_read_with_child_with_size, prove_read, + read_range_proof_check_with_child_on_proving_backend, Backend as StateBackend, + ChildStorageCollection, KeyValueStates, KeyValueStorageLevel, StorageCollection, + MAX_NESTED_TRIE_DEPTH, +}; +use sp_trie::{proof_size_extension::ProofSizeExt, CompactProof, MerkleValue, StorageProof}; +use std::{ + collections::{HashMap, HashSet}, + marker::PhantomData, + path::PathBuf, + sync::Arc, +}; + +#[cfg(feature = "test-helpers")] +use { + super::call_executor::LocalCallExecutor, sc_client_api::in_mem, sp_core::traits::CodeExecutor, +}; + +type NotificationSinks = Mutex>>; + +/// Substrate Client +pub struct Client +where + Block: BlockT, +{ + backend: Arc, + executor: E, + storage_notifications: StorageNotifications, + import_notification_sinks: NotificationSinks>, + every_import_notification_sinks: NotificationSinks>, + finality_notification_sinks: NotificationSinks>, + // Collects auxiliary operations to be performed atomically together with + // block import operations. + import_actions: Mutex>>, + // Collects auxiliary operations to be performed atomically together with + // block finalization operations. + finality_actions: Mutex>>, + // Holds the block hash currently being imported. TODO: replace this with block queue. + importing_block: RwLock>, + block_rules: BlockRules, + config: ClientConfig, + telemetry: Option, + unpin_worker_sender: TracingUnboundedSender>, + code_provider: CodeProvider, + _phantom: PhantomData, +} + +/// Used in importing a block, where additional changes are made after the runtime +/// executed. +enum PrePostHeader { + /// they are the same: no post-runtime digest items. + Same(H), + /// different headers (pre, post). + Different(H, H), +} + +impl PrePostHeader { + /// get a reference to the "post-header" -- the header as it should be + /// after all changes are applied. + fn post(&self) -> &H { + match *self { + PrePostHeader::Same(ref h) => h, + PrePostHeader::Different(_, ref h) => h, + } + } + + /// convert to the "post-header" -- the header as it should be after + /// all changes are applied. + fn into_post(self) -> H { + match self { + PrePostHeader::Same(h) => h, + PrePostHeader::Different(_, h) => h, + } + } +} + +enum PrepareStorageChangesResult { + Discard(ImportResult), + Import(Option>), +} + +/// Create an instance of in-memory client. +#[cfg(feature = "test-helpers")] +pub fn new_in_mem( + backend: Arc>, + executor: E, + genesis_block_builder: G, + prometheus_registry: Option, + telemetry: Option, + spawn_handle: Box, + config: ClientConfig, +) -> sp_blockchain::Result< + Client, LocalCallExecutor, E>, Block, RA>, +> +where + E: CodeExecutor + sc_executor::RuntimeVersionOf, + Block: BlockT, + G: BuildGenesisBlock< + Block, + BlockImportOperation = as backend::Backend>::BlockImportOperation, + >, +{ + new_with_backend( + backend, + executor, + genesis_block_builder, + spawn_handle, + prometheus_registry, + telemetry, + config, + ) +} + +/// Client configuration items. +#[derive(Debug, Clone)] +pub struct ClientConfig { + /// Enable the offchain worker db. + pub offchain_worker_enabled: bool, + /// If true, allows access from the runtime to write into offchain worker db. + pub offchain_indexing_api: bool, + /// Path where WASM files exist to override the on-chain WASM. + pub wasm_runtime_overrides: Option, + /// Skip writing genesis state on first start. + pub no_genesis: bool, + /// Map of WASM runtime substitute starting at the child of the given block until the runtime + /// version doesn't match anymore. + pub wasm_runtime_substitutes: HashMap, Vec>, + /// Enable recording of storage proofs during block import + pub enable_import_proof_recording: bool, +} + +impl Default for ClientConfig { + fn default() -> Self { + Self { + offchain_worker_enabled: false, + offchain_indexing_api: false, + wasm_runtime_overrides: None, + no_genesis: false, + wasm_runtime_substitutes: HashMap::new(), + enable_import_proof_recording: false, + } + } +} + +/// Create a client with the explicitly provided backend. +/// This is useful for testing backend implementations. +#[cfg(feature = "test-helpers")] +pub fn new_with_backend( + backend: Arc, + executor: E, + genesis_block_builder: G, + spawn_handle: Box, + prometheus_registry: Option, + telemetry: Option, + config: ClientConfig, +) -> sp_blockchain::Result, Block, RA>> +where + E: CodeExecutor + sc_executor::RuntimeVersionOf, + G: BuildGenesisBlock< + Block, + BlockImportOperation = >::BlockImportOperation, + >, + Block: BlockT, + B: backend::LocalBackend + 'static, +{ + let extensions = ExecutionExtensions::new(None, Arc::new(executor.clone())); + + let call_executor = + LocalCallExecutor::new(backend.clone(), executor, config.clone(), extensions)?; + + Client::new( + backend, + call_executor, + spawn_handle, + genesis_block_builder, + Default::default(), + Default::default(), + prometheus_registry, + telemetry, + config, + ) +} + +impl BlockOf for Client +where + B: backend::Backend, + E: CallExecutor, + Block: BlockT, +{ + type Type = Block; +} + +impl LockImportRun for Client +where + B: backend::Backend, + E: CallExecutor, + Block: BlockT, +{ + fn lock_import_and_run(&self, f: F) -> Result + where + F: FnOnce(&mut ClientImportOperation) -> Result, + Err: From, + { + let inner = || { + let _import_lock = self.backend.get_import_lock().write(); + + let mut op = ClientImportOperation { + op: self.backend.begin_operation()?, + notify_imported: None, + notify_finalized: None, + }; + + let r = f(&mut op)?; + + let ClientImportOperation { + mut op, + notify_imported, + notify_finalized, + } = op; + + let finality_notification = notify_finalized.map(|summary| { + FinalityNotification::from_summary(summary, self.unpin_worker_sender.clone()) + }); + + let (import_notification, storage_changes, import_notification_action) = + match notify_imported { + Some(mut summary) => { + let import_notification_action = summary.import_notification_action; + let storage_changes = summary.storage_changes.take(); + ( + Some(BlockImportNotification::from_summary( + summary, + self.unpin_worker_sender.clone(), + )), + storage_changes, + import_notification_action, + ) + } + None => (None, None, ImportNotificationAction::None), + }; + + if let Some(ref notification) = finality_notification { + for action in self.finality_actions.lock().iter_mut() { + op.insert_aux(action(notification))?; + } + } + if let Some(ref notification) = import_notification { + for action in self.import_actions.lock().iter_mut() { + op.insert_aux(action(notification))?; + } + } + + self.backend.commit_operation(op)?; + + // We need to pin the block in the backend once + // for each notification. Once all notifications are + // dropped, the block will be unpinned automatically. + if let Some(ref notification) = finality_notification { + if let Err(err) = self.backend.pin_block(notification.hash) { + debug!( + "Unable to pin block for finality notification. hash: {}, Error: {}", + notification.hash, err + ); + } else { + let _ = self + .unpin_worker_sender + .unbounded_send(UnpinWorkerMessage::AnnouncePin(notification.hash)) + .map_err(|e| { + log::error!( + "Unable to send AnnouncePin worker message for finality: {e}" + ) + }); + } + } + + if let Some(ref notification) = import_notification { + if let Err(err) = self.backend.pin_block(notification.hash) { + debug!( + "Unable to pin block for import notification. hash: {}, Error: {}", + notification.hash, err + ); + } else { + let _ = self + .unpin_worker_sender + .unbounded_send(UnpinWorkerMessage::AnnouncePin(notification.hash)) + .map_err(|e| { + log::error!("Unable to send AnnouncePin worker message for import: {e}") + }); + }; + } + + self.notify_finalized(finality_notification)?; + self.notify_imported( + import_notification, + import_notification_action, + storage_changes, + )?; + + Ok(r) + }; + + let result = inner(); + *self.importing_block.write() = None; + + result + } +} + +impl LockImportRun for &Client +where + Block: BlockT, + B: backend::Backend, + E: CallExecutor, +{ + fn lock_import_and_run(&self, f: F) -> Result + where + F: FnOnce(&mut ClientImportOperation) -> Result, + Err: From, + { + (**self).lock_import_and_run(f) + } +} + +impl Client +where + B: backend::Backend, + E: CallExecutor, + Block: BlockT, + Block::Header: Clone, +{ + /// Creates new Substrate Client with given blockchain and code executor. + pub fn new( + backend: Arc, + executor: E, + spawn_handle: Box, + genesis_block_builder: G, + fork_blocks: ForkBlocks, + bad_blocks: BadBlocks, + prometheus_registry: Option, + telemetry: Option, + config: ClientConfig, + ) -> sp_blockchain::Result + where + G: BuildGenesisBlock< + Block, + BlockImportOperation = >::BlockImportOperation, + >, + E: Clone, + B: 'static, + { + let info = backend.blockchain().info(); + if info.finalized_state.is_none() { + let (genesis_block, mut op) = genesis_block_builder.build_genesis_block()?; + info!( + "🔨 Initializing Genesis block/state (state: {}, header-hash: {})", + genesis_block.header().state_root(), + genesis_block.header().hash() + ); + // Genesis may be written after some blocks have been imported and finalized. + // So we only finalize it when the database is empty. + let block_state = if info.best_hash == Default::default() { + NewBlockState::Final + } else { + NewBlockState::Normal + }; + let (header, body) = genesis_block.deconstruct(); + op.set_block_data(header, Some(body), None, None, block_state)?; + backend.commit_operation(op)?; + } + + let (unpin_worker_sender, rx) = tracing_unbounded::>( + "notification-pinning-worker-channel", + 10_000, + ); + let unpin_worker = NotificationPinningWorker::new(rx, backend.clone()); + spawn_handle.spawn( + "notification-pinning-worker", + None, + Box::pin(unpin_worker.run()), + ); + let code_provider = CodeProvider::new(&config, executor.clone(), backend.clone())?; + + Ok(Client { + backend, + executor, + storage_notifications: StorageNotifications::new(prometheus_registry), + import_notification_sinks: Default::default(), + every_import_notification_sinks: Default::default(), + finality_notification_sinks: Default::default(), + import_actions: Default::default(), + finality_actions: Default::default(), + importing_block: Default::default(), + block_rules: BlockRules::new(fork_blocks, bad_blocks), + config, + telemetry, + unpin_worker_sender, + code_provider, + _phantom: Default::default(), + }) + } + + /// returns a reference to the block import notification sinks + /// useful for test environments. + pub fn import_notification_sinks(&self) -> &NotificationSinks> { + &self.import_notification_sinks + } + + /// returns a reference to the finality notification sinks + /// useful for test environments. + pub fn finality_notification_sinks(&self) -> &NotificationSinks> { + &self.finality_notification_sinks + } + + /// Get a reference to the state at a given block. + pub fn state_at(&self, hash: Block::Hash) -> sp_blockchain::Result { + self.backend.state_at(hash) + } + + /// Get the code at a given block. + /// + /// This takes any potential substitutes into account, but ignores overrides. + pub fn code_at(&self, hash: Block::Hash) -> sp_blockchain::Result> { + self.code_provider.code_at_ignoring_overrides(hash) + } + + /// Get the RuntimeVersion at a given block. + pub fn runtime_version_at(&self, hash: Block::Hash) -> sp_blockchain::Result { + CallExecutor::runtime_version(&self.executor, hash) + } + + /// Apply a checked and validated block to an operation. + fn apply_block( + &self, + operation: &mut ClientImportOperation, + import_block: BlockImportParams, + storage_changes: Option>, + ) -> sp_blockchain::Result + where + Self: ProvideRuntimeApi, + >::Api: CoreApi + ApiExt, + { + let BlockImportParams { + origin, + header, + justifications, + post_digests, + body, + indexed_body, + finalized, + auxiliary, + fork_choice, + intermediates, + import_existing, + .. + } = import_block; + + if !intermediates.is_empty() { + return Err(Error::IncompletePipeline); + } + + let fork_choice = fork_choice.ok_or(Error::IncompletePipeline)?; + + let import_headers = if post_digests.is_empty() { + PrePostHeader::Same(header) + } else { + let mut post_header = header.clone(); + for item in post_digests { + post_header.digest_mut().push(item); + } + PrePostHeader::Different(header, post_header) + }; + + let hash = import_headers.post().hash(); + let height = (*import_headers.post().number()).saturated_into::(); + + *self.importing_block.write() = Some(hash); + + let result = self.execute_and_import_block( + operation, + origin, + hash, + import_headers, + justifications, + body, + indexed_body, + storage_changes, + finalized, + auxiliary, + fork_choice, + import_existing, + ); + + if let Ok(ImportResult::Imported(ref aux)) = result { + if aux.is_new_best { + // don't send telemetry block import events during initial sync for every + // block to avoid spamming the telemetry server, these events will be randomly + // sent at a rate of 1/10. + if origin != BlockOrigin::NetworkInitialSync || rand::thread_rng().gen_bool(0.1) { + telemetry!( + self.telemetry; + SUBSTRATE_INFO; + "block.import"; + "height" => height, + "best" => ?hash, + "origin" => ?origin + ); + } + } + } + + result + } + + fn execute_and_import_block( + &self, + operation: &mut ClientImportOperation, + origin: BlockOrigin, + hash: Block::Hash, + import_headers: PrePostHeader, + justifications: Option, + body: Option>, + indexed_body: Option>>, + storage_changes: Option>, + finalized: bool, + aux: Vec<(Vec, Option>)>, + fork_choice: ForkChoiceStrategy, + import_existing: bool, + ) -> sp_blockchain::Result + where + Self: ProvideRuntimeApi, + >::Api: CoreApi + ApiExt, + { + let parent_hash = *import_headers.post().parent_hash(); + let status = self.backend.blockchain().status(hash)?; + let parent_exists = + self.backend.blockchain().status(parent_hash)? == blockchain::BlockStatus::InChain; + match (import_existing, status) { + (false, blockchain::BlockStatus::InChain) => return Ok(ImportResult::AlreadyInChain), + (false, blockchain::BlockStatus::Unknown) => {} + (true, blockchain::BlockStatus::InChain) => {} + (true, blockchain::BlockStatus::Unknown) => {} + } + + let info = self.backend.blockchain().info(); + let gap_block = info + .block_gap + .is_some_and(|(start, _)| *import_headers.post().number() == start); + + // the block is lower than our last finalized block so it must revert + // finality, refusing import. + if status == blockchain::BlockStatus::Unknown + && *import_headers.post().number() <= info.finalized_number + && !gap_block + { + return Err(sp_blockchain::Error::NotInFinalizedChain); + } + + // this is a fairly arbitrary choice of where to draw the line on making notifications, + // but the general goal is to only make notifications when we are already fully synced + // and get a new chain head. + let make_notifications = match origin { + BlockOrigin::NetworkBroadcast | BlockOrigin::Own | BlockOrigin::ConsensusBroadcast => { + true + } + BlockOrigin::Genesis | BlockOrigin::NetworkInitialSync | BlockOrigin::File => false, + }; + + let storage_changes = match storage_changes { + Some(storage_changes) => { + let storage_changes = match storage_changes { + sc_consensus::StorageChanges::Changes(storage_changes) => { + self.backend + .begin_state_operation(&mut operation.op, parent_hash)?; + let (main_sc, child_sc, offchain_sc, tx, _, tx_index) = + storage_changes.into_inner(); + + if self.config.offchain_indexing_api { + operation.op.update_offchain_storage(offchain_sc)?; + } + + operation.op.update_db_storage(tx)?; + operation + .op + .update_storage(main_sc.clone(), child_sc.clone())?; + operation.op.update_transaction_index(tx_index)?; + + Some((main_sc, child_sc)) + } + sc_consensus::StorageChanges::Import(changes) => { + let mut storage = sp_storage::Storage::default(); + for state in changes.state.0.into_iter() { + if state.parent_storage_keys.is_empty() && state.state_root.is_empty() { + for (key, value) in state.key_values.into_iter() { + storage.top.insert(key, value); + } + } else { + for parent_storage in state.parent_storage_keys { + let storage_key = PrefixedStorageKey::new_ref(&parent_storage); + let storage_key = + match ChildType::from_prefixed_key(storage_key) { + Some((ChildType::ParentKeyId, storage_key)) => { + storage_key + } + None => { + return Err(Error::Backend( + "Invalid child storage key.".to_string(), + )) + } + }; + let entry = storage + .children_default + .entry(storage_key.to_vec()) + .or_insert_with(|| StorageChild { + data: Default::default(), + child_info: ChildInfo::new_default(storage_key), + }); + for (key, value) in state.key_values.iter() { + entry.data.insert(key.clone(), value.clone()); + } + } + } + } + + // This is use by fast sync for runtime version to be resolvable from + // changes. + let state_version = resolve_state_version_from_wasm::<_, HashingFor>( + &storage, + &self.executor, + )?; + let state_root = operation.op.reset_storage(storage, state_version)?; + if state_root != *import_headers.post().state_root() { + // State root mismatch when importing state. This should not happen in + // safe fast sync mode, but may happen in unsafe mode. + warn!("Error importing state: State root mismatch."); + return Err(Error::InvalidStateRoot); + } + None + } + }; + + storage_changes + } + None => None, + }; + + // Ensure parent chain is finalized to maintain invariant that finality is called + // sequentially. + if finalized && parent_exists && info.finalized_hash != parent_hash { + self.apply_finality_with_block_hash( + operation, + parent_hash, + None, + &info, + make_notifications, + )?; + } + + let is_new_best = !gap_block + && (finalized + || match fork_choice { + ForkChoiceStrategy::LongestChain => { + import_headers.post().number() > &info.best_number + } + ForkChoiceStrategy::Custom(v) => v, + }); + + let leaf_state = if finalized { + NewBlockState::Final + } else if is_new_best { + NewBlockState::Best + } else { + NewBlockState::Normal + }; + + let tree_route = if is_new_best && info.best_hash != parent_hash && parent_exists { + let route_from_best = + sp_blockchain::tree_route(self.backend.blockchain(), info.best_hash, parent_hash)?; + Some(route_from_best) + } else { + None + }; + + trace!( + "Imported {}, (#{}), best={}, origin={:?}", + hash, + import_headers.post().number(), + is_new_best, + origin, + ); + + operation.op.set_block_data( + import_headers.post().clone(), + body, + indexed_body, + justifications, + leaf_state, + )?; + + operation.op.insert_aux(aux)?; + + let should_notify_every_block = !self.every_import_notification_sinks.lock().is_empty(); + + // Notify when we are already synced to the tip of the chain + // or if this import triggers a re-org + let should_notify_recent_block = make_notifications || tree_route.is_some(); + + if should_notify_every_block || should_notify_recent_block { + let header = import_headers.into_post(); + if finalized && should_notify_recent_block { + let mut summary = match operation.notify_finalized.take() { + Some(mut summary) => { + summary.header = header.clone(); + summary.finalized.push(hash); + summary + } + None => FinalizeSummary { + header: header.clone(), + finalized: vec![hash], + stale_heads: Vec::new(), + }, + }; + + if parent_exists { + // Add to the stale list all heads that are branching from parent besides our + // current `head`. + for head in self + .backend + .blockchain() + .leaves()? + .into_iter() + .filter(|h| *h != parent_hash) + { + let route_from_parent = sp_blockchain::tree_route( + self.backend.blockchain(), + parent_hash, + head, + )?; + if route_from_parent.retracted().is_empty() { + summary.stale_heads.push(head); + } + } + } + operation.notify_finalized = Some(summary); + } + + let import_notification_action = if should_notify_every_block { + if should_notify_recent_block { + ImportNotificationAction::Both + } else { + ImportNotificationAction::EveryBlock + } + } else { + ImportNotificationAction::RecentBlock + }; + + operation.notify_imported = Some(ImportSummary { + hash, + origin, + header, + is_new_best, + storage_changes, + tree_route, + import_notification_action, + }) + } + + Ok(ImportResult::imported(is_new_best)) + } + + /// Prepares the storage changes for a block. + /// + /// It checks if the state should be enacted and if the `import_block` maybe already provides + /// the required storage changes. If the state should be enacted and the storage changes are not + /// provided, the block is re-executed to get the storage changes. + fn prepare_block_storage_changes( + &self, + import_block: &mut BlockImportParams, + ) -> sp_blockchain::Result> + where + Self: ProvideRuntimeApi, + >::Api: CoreApi + ApiExt, + { + let parent_hash = import_block.header.parent_hash(); + let state_action = std::mem::replace(&mut import_block.state_action, StateAction::Skip); + let (enact_state, storage_changes) = match (self.block_status(*parent_hash)?, state_action) + { + (BlockStatus::KnownBad, _) => { + return Ok(PrepareStorageChangesResult::Discard(ImportResult::KnownBad)) + } + ( + BlockStatus::InChainPruned, + StateAction::ApplyChanges(sc_consensus::StorageChanges::Changes(_)), + ) => { + return Ok(PrepareStorageChangesResult::Discard( + ImportResult::MissingState, + )) + } + (_, StateAction::ApplyChanges(changes)) => (true, Some(changes)), + (BlockStatus::Unknown, _) => { + return Ok(PrepareStorageChangesResult::Discard( + ImportResult::UnknownParent, + )) + } + (_, StateAction::Skip) => (false, None), + (BlockStatus::InChainPruned, StateAction::Execute) => { + return Ok(PrepareStorageChangesResult::Discard( + ImportResult::MissingState, + )) + } + (BlockStatus::InChainPruned, StateAction::ExecuteIfPossible) => (false, None), + (_, StateAction::Execute) => (true, None), + (_, StateAction::ExecuteIfPossible) => (true, None), + }; + + let storage_changes = match (enact_state, storage_changes, &import_block.body) { + // We have storage changes and should enact the state, so we don't need to do anything + // here + (true, changes @ Some(_), _) => changes, + // We should enact state, but don't have any storage changes, so we need to execute the + // block. + (true, None, Some(ref body)) => { + let mut runtime_api = self.runtime_api(); + + runtime_api.set_call_context(CallContext::Onchain); + + if self.config.enable_import_proof_recording { + runtime_api.record_proof(); + let recorder = runtime_api + .proof_recorder() + .expect("Proof recording is enabled in the line above; qed."); + runtime_api.register_extension(ProofSizeExt::new(recorder)); + } + + runtime_api.execute_block( + *parent_hash, + Block::new(import_block.header.clone(), body.clone()), + )?; + + let state = self.backend.state_at(*parent_hash)?; + let gen_storage_changes = runtime_api + .into_storage_changes(&state, *parent_hash) + .map_err(sp_blockchain::Error::Storage)?; + + if import_block.header.state_root() != &gen_storage_changes.transaction_storage_root + { + return Err(Error::InvalidStateRoot); + } + Some(sc_consensus::StorageChanges::Changes(gen_storage_changes)) + } + // No block body, no storage changes + (true, None, None) => None, + // We should not enact the state, so we set the storage changes to `None`. + (false, _, _) => None, + }; + + Ok(PrepareStorageChangesResult::Import(storage_changes)) + } + + fn apply_finality_with_block_hash( + &self, + operation: &mut ClientImportOperation, + hash: Block::Hash, + justification: Option, + info: &BlockchainInfo, + notify: bool, + ) -> sp_blockchain::Result<()> { + if hash == info.finalized_hash { + warn!( + "Possible safety violation: attempted to re-finalize last finalized block {:?} ", + hash, + ); + return Ok(()); + } + + // Find tree route from last finalized to given block. + let route_from_finalized = + sp_blockchain::tree_route(self.backend.blockchain(), info.finalized_hash, hash)?; + + if let Some(retracted) = route_from_finalized.retracted().first() { + warn!( + "Safety violation: attempted to revert finalized block {:?} which is not in the \ + same chain as last finalized {:?}", + retracted, info.finalized_hash + ); + + return Err(sp_blockchain::Error::NotInFinalizedChain); + } + + // We may need to coercively update the best block if there is more than one + // leaf or if the finalized block number is greater than last best number recorded + // by the backend. This last condition may apply in case of consensus implementations + // not always checking this condition. + let block_number = self + .backend + .blockchain() + .number(hash)? + .ok_or(Error::MissingHeader(format!("{hash:?}")))?; + if self.backend.blockchain().leaves()?.len() > 1 || info.best_number < block_number { + let route_from_best = + sp_blockchain::tree_route(self.backend.blockchain(), info.best_hash, hash)?; + + // If the block is not a direct ancestor of the current best chain, + // then some other block is the common ancestor. + if route_from_best.common_block().hash != hash { + // NOTE: we're setting the finalized block as best block, this might + // be slightly inaccurate since we might have a "better" block + // further along this chain, but since best chain selection logic is + // pluggable we cannot make a better choice here. usages that need + // an accurate "best" block need to go through `SelectChain` + // instead. + operation.op.mark_head(hash)?; + } + } + + let enacted = route_from_finalized.enacted(); + assert!(!enacted.is_empty()); + for finalize_new in &enacted[..enacted.len() - 1] { + operation.op.mark_finalized(finalize_new.hash, None)?; + } + + assert_eq!(enacted.last().map(|e| e.hash), Some(hash)); + operation.op.mark_finalized(hash, justification)?; + + if notify { + let finalized = route_from_finalized + .enacted() + .iter() + .map(|elem| elem.hash) + .collect::>(); + + let block_number = route_from_finalized + .last() + .expect( + "The block to finalize is always the latest \ + block in the route to the finalized block; qed", + ) + .number; + + // The stale heads are the leaves that will be displaced after the + // block is finalized. + let stale_heads = self + .backend + .blockchain() + .displaced_leaves_after_finalizing(hash, block_number)? + .hashes() + .collect(); + + let header = self + .backend + .blockchain() + .header(hash)? + .expect("Block to finalize expected to be onchain; qed"); + + operation.notify_finalized = Some(FinalizeSummary { + header, + finalized, + stale_heads, + }); + } + + Ok(()) + } + + fn notify_finalized( + &self, + notification: Option>, + ) -> sp_blockchain::Result<()> { + let mut sinks = self.finality_notification_sinks.lock(); + + let notification = match notification { + Some(notify_finalized) => notify_finalized, + None => { + // Cleanup any closed finality notification sinks + // since we won't be running the loop below which + // would also remove any closed sinks. + sinks.retain(|sink| !sink.is_closed()); + return Ok(()); + } + }; + + telemetry!( + self.telemetry; + SUBSTRATE_INFO; + "notify.finalized"; + "height" => format!("{}", notification.header.number()), + "best" => ?notification.hash, + ); + + sinks.retain(|sink| sink.unbounded_send(notification.clone()).is_ok()); + + Ok(()) + } + + fn notify_imported( + &self, + notification: Option>, + import_notification_action: ImportNotificationAction, + storage_changes: Option<(StorageCollection, ChildStorageCollection)>, + ) -> sp_blockchain::Result<()> { + let notification = match notification { + Some(notify_import) => notify_import, + None => { + // Cleanup any closed import notification sinks since we won't + // be sending any notifications below which would remove any + // closed sinks. this is necessary since during initial sync we + // won't send any import notifications which could lead to a + // temporary leak of closed/discarded notification sinks (e.g. + // from consensus code). + self.import_notification_sinks + .lock() + .retain(|sink| !sink.is_closed()); + + self.every_import_notification_sinks + .lock() + .retain(|sink| !sink.is_closed()); + + return Ok(()); + } + }; + + let trigger_storage_changes_notification = || { + if let Some(storage_changes) = storage_changes { + // TODO [ToDr] How to handle re-orgs? Should we re-emit all storage changes? + self.storage_notifications.trigger( + ¬ification.hash, + storage_changes.0.into_iter(), + storage_changes + .1 + .into_iter() + .map(|(sk, v)| (sk, v.into_iter())), + ); + } + }; + + match import_notification_action { + ImportNotificationAction::Both => { + trigger_storage_changes_notification(); + self.import_notification_sinks + .lock() + .retain(|sink| sink.unbounded_send(notification.clone()).is_ok()); + + self.every_import_notification_sinks + .lock() + .retain(|sink| sink.unbounded_send(notification.clone()).is_ok()); + } + ImportNotificationAction::RecentBlock => { + trigger_storage_changes_notification(); + self.import_notification_sinks + .lock() + .retain(|sink| sink.unbounded_send(notification.clone()).is_ok()); + + self.every_import_notification_sinks + .lock() + .retain(|sink| !sink.is_closed()); + } + ImportNotificationAction::EveryBlock => { + self.every_import_notification_sinks + .lock() + .retain(|sink| sink.unbounded_send(notification.clone()).is_ok()); + + self.import_notification_sinks + .lock() + .retain(|sink| !sink.is_closed()); + } + ImportNotificationAction::None => { + // This branch is unreachable in fact because the block import notification must be + // Some(_) instead of None (it's already handled at the beginning of this function) + // at this point. + self.import_notification_sinks + .lock() + .retain(|sink| !sink.is_closed()); + + self.every_import_notification_sinks + .lock() + .retain(|sink| !sink.is_closed()); + } + } + + Ok(()) + } + + /// Attempts to revert the chain by `n` blocks guaranteeing that no block is + /// reverted past the last finalized block. Returns the number of blocks + /// that were successfully reverted. + pub fn revert(&self, n: NumberFor) -> sp_blockchain::Result> { + let (number, _) = self.backend.revert(n, false)?; + Ok(number) + } + + /// Attempts to revert the chain by `n` blocks disregarding finality. This method will revert + /// any finalized blocks as requested and can potentially leave the node in an inconsistent + /// state. Other modules in the system that persist data and that rely on finality + /// (e.g. consensus parts) will be unaffected by the revert. Use this method with caution and + /// making sure that no other data needs to be reverted for consistency aside from the block + /// data. If `blacklist` is set to true, will also blacklist reverted blocks from finalizing + /// again. The blacklist is reset upon client restart. + /// + /// Returns the number of blocks that were successfully reverted. + pub fn unsafe_revert( + &mut self, + n: NumberFor, + blacklist: bool, + ) -> sp_blockchain::Result> { + let (number, reverted) = self.backend.revert(n, true)?; + if blacklist { + for b in reverted { + self.block_rules.mark_bad(b); + } + } + Ok(number) + } + + /// Get blockchain info. + pub fn chain_info(&self) -> BlockchainInfo { + self.backend.blockchain().info() + } + + /// Get block status. + pub fn block_status(&self, hash: Block::Hash) -> sp_blockchain::Result { + // this can probably be implemented more efficiently + if self.importing_block.read().as_ref() == Some(&hash) { + return Ok(BlockStatus::Queued); + } + + let hash_and_number = self.backend.blockchain().number(hash)?.map(|n| (hash, n)); + match hash_and_number { + Some((hash, number)) => { + if self.backend.have_state_at(hash, number) { + Ok(BlockStatus::InChainWithState) + } else { + Ok(BlockStatus::InChainPruned) + } + } + None => Ok(BlockStatus::Unknown), + } + } + + /// Get block header by id. + pub fn header( + &self, + hash: Block::Hash, + ) -> sp_blockchain::Result::Header>> { + self.backend.blockchain().header(hash) + } + + /// Get block body by id. + pub fn body( + &self, + hash: Block::Hash, + ) -> sp_blockchain::Result::Extrinsic>>> { + self.backend.blockchain().body(hash) + } + + /// Gets the uncles of the block with `target_hash` going back `max_generation` ancestors. + pub fn uncles( + &self, + target_hash: Block::Hash, + max_generation: NumberFor, + ) -> sp_blockchain::Result> { + let load_header = |hash: Block::Hash| -> sp_blockchain::Result { + self.backend + .blockchain() + .header(hash)? + .ok_or_else(|| Error::UnknownBlock(format!("{:?}", hash))) + }; + + let genesis_hash = self.backend.blockchain().info().genesis_hash; + if genesis_hash == target_hash { + return Ok(Vec::new()); + } + + let mut current_hash = target_hash; + let mut current = load_header(current_hash)?; + let mut ancestor_hash = *current.parent_hash(); + let mut ancestor = load_header(ancestor_hash)?; + let mut uncles = Vec::new(); + + let mut generation: NumberFor = Zero::zero(); + while generation < max_generation { + let children = self.backend.blockchain().children(ancestor_hash)?; + uncles.extend(children.into_iter().filter(|h| h != ¤t_hash)); + current_hash = ancestor_hash; + + if genesis_hash == current_hash { + break; + } + + current = ancestor; + ancestor_hash = *current.parent_hash(); + ancestor = load_header(ancestor_hash)?; + generation += One::one(); + } + trace!("Collected {} uncles", uncles.len()); + Ok(uncles) + } +} + +impl UsageProvider for Client +where + B: backend::Backend, + E: CallExecutor, + Block: BlockT, +{ + /// Get usage info about current client. + fn usage_info(&self) -> ClientInfo { + ClientInfo { + chain: self.chain_info(), + usage: self.backend.usage_info(), + } + } +} + +impl ProofProvider for Client +where + B: backend::Backend, + E: CallExecutor, + Block: BlockT, +{ + fn read_proof( + &self, + hash: Block::Hash, + keys: &mut dyn Iterator, + ) -> sp_blockchain::Result { + self.state_at(hash) + .and_then(|state| prove_read(state, keys).map_err(Into::into)) + } + + fn read_child_proof( + &self, + hash: Block::Hash, + child_info: &ChildInfo, + keys: &mut dyn Iterator, + ) -> sp_blockchain::Result { + self.state_at(hash) + .and_then(|state| prove_child_read(state, child_info, keys).map_err(Into::into)) + } + + fn execution_proof( + &self, + hash: Block::Hash, + method: &str, + call_data: &[u8], + ) -> sp_blockchain::Result<(Vec, StorageProof)> { + self.executor.prove_execution(hash, method, call_data) + } + + fn read_proof_collection( + &self, + hash: Block::Hash, + start_key: &[Vec], + size_limit: usize, + ) -> sp_blockchain::Result<(CompactProof, u32)> { + let state = self.state_at(hash)?; + // this is a read proof, using version V0 or V1 is equivalent. + let root = state.storage_root(std::iter::empty(), StateVersion::V0).0; + + let (proof, count) = prove_range_read_with_child_with_size::<_, HashingFor>( + state, size_limit, start_key, + )?; + let proof = proof + .into_compact_proof::>(root) + .map_err(|e| sp_blockchain::Error::from_state(Box::new(e)))?; + Ok((proof, count)) + } + + fn storage_collection( + &self, + hash: Block::Hash, + start_key: &[Vec], + size_limit: usize, + ) -> sp_blockchain::Result> { + if start_key.len() > MAX_NESTED_TRIE_DEPTH { + return Err(Error::Backend("Invalid start key.".to_string())); + } + let state = self.state_at(hash)?; + let child_info = |storage_key: &Vec| -> sp_blockchain::Result { + let storage_key = PrefixedStorageKey::new_ref(storage_key); + match ChildType::from_prefixed_key(storage_key) { + Some((ChildType::ParentKeyId, storage_key)) => { + Ok(ChildInfo::new_default(storage_key)) + } + None => Err(Error::Backend("Invalid child storage key.".to_string())), + } + }; + let mut current_child = if start_key.len() == 2 { + let start_key = start_key.first().expect("checked len"); + if let Some(child_root) = state + .storage(start_key) + .map_err(|e| sp_blockchain::Error::from_state(Box::new(e)))? + { + Some((child_info(start_key)?, child_root)) + } else { + return Err(Error::Backend("Invalid root start key.".to_string())); + } + } else { + None + }; + let mut current_key = start_key.last().cloned().unwrap_or_default(); + let mut total_size = 0; + let mut result = vec![( + KeyValueStorageLevel { + state_root: Vec::new(), + key_values: Vec::new(), + parent_storage_keys: Vec::new(), + }, + false, + )]; + + let mut child_roots = HashSet::new(); + loop { + let mut entries = Vec::new(); + let mut complete = true; + let mut switch_child_key = None; + while let Some(next_key) = if let Some(child) = current_child.as_ref() { + state + .next_child_storage_key(&child.0, ¤t_key) + .map_err(|e| sp_blockchain::Error::from_state(Box::new(e)))? + } else { + state + .next_storage_key(¤t_key) + .map_err(|e| sp_blockchain::Error::from_state(Box::new(e)))? + } { + let value = if let Some(child) = current_child.as_ref() { + state + .child_storage(&child.0, next_key.as_ref()) + .map_err(|e| sp_blockchain::Error::from_state(Box::new(e)))? + .unwrap_or_default() + } else { + state + .storage(next_key.as_ref()) + .map_err(|e| sp_blockchain::Error::from_state(Box::new(e)))? + .unwrap_or_default() + }; + let size = value.len() + next_key.len(); + if total_size + size > size_limit && !entries.is_empty() { + complete = false; + break; + } + total_size += size; + + if current_child.is_none() + && sp_core::storage::well_known_keys::is_child_storage_key(next_key.as_slice()) + && !child_roots.contains(value.as_slice()) + { + child_roots.insert(value.clone()); + switch_child_key = Some((next_key.clone(), value.clone())); + entries.push((next_key.clone(), value)); + break; + } + entries.push((next_key.clone(), value)); + current_key = next_key; + } + if let Some((child, child_root)) = switch_child_key.take() { + result[0].0.key_values.extend(entries); + current_child = Some((child_info(&child)?, child_root)); + current_key = Vec::new(); + } else if let Some((child, child_root)) = current_child.take() { + current_key = child.into_prefixed_storage_key().into_inner(); + result.push(( + KeyValueStorageLevel { + state_root: child_root, + key_values: entries, + parent_storage_keys: Vec::new(), + }, + complete, + )); + if !complete { + break; + } + } else { + result[0].0.key_values.extend(entries); + result[0].1 = complete; + break; + } + } + Ok(result) + } + + fn verify_range_proof( + &self, + root: Block::Hash, + proof: CompactProof, + start_key: &[Vec], + ) -> sp_blockchain::Result<(KeyValueStates, usize)> { + let mut db = sp_state_machine::MemoryDB::>::new(&[]); + // Compact encoding + let _ = sp_trie::decode_compact::>, _, _>( + &mut db, + proof.iter_compact_encoded_nodes(), + Some(&root), + ) + .map_err(|e| sp_blockchain::Error::from_state(Box::new(e)))?; + let proving_backend = sp_state_machine::TrieBackendBuilder::new(db, root).build(); + let state = read_range_proof_check_with_child_on_proving_backend::>( + &proving_backend, + start_key, + )?; + + Ok(state) + } +} + +impl ExecutorProvider for Client +where + B: backend::Backend, + E: CallExecutor, + Block: BlockT, +{ + type Executor = E; + + fn executor(&self) -> &Self::Executor { + &self.executor + } + + fn execution_extensions(&self) -> &ExecutionExtensions { + self.executor.execution_extensions() + } +} + +impl StorageProvider for Client +where + B: backend::Backend, + E: CallExecutor, + Block: BlockT, +{ + fn storage_keys( + &self, + hash: ::Hash, + prefix: Option<&StorageKey>, + start_key: Option<&StorageKey>, + ) -> sp_blockchain::Result> { + let state = self.state_at(hash)?; + KeysIter::new(state, prefix, start_key) + .map_err(|e| sp_blockchain::Error::from_state(Box::new(e))) + } + + fn child_storage_keys( + &self, + hash: ::Hash, + child_info: ChildInfo, + prefix: Option<&StorageKey>, + start_key: Option<&StorageKey>, + ) -> sp_blockchain::Result> { + let state = self.state_at(hash)?; + KeysIter::new_child(state, child_info, prefix, start_key) + .map_err(|e| sp_blockchain::Error::from_state(Box::new(e))) + } + + fn storage_pairs( + &self, + hash: ::Hash, + prefix: Option<&StorageKey>, + start_key: Option<&StorageKey>, + ) -> sp_blockchain::Result> { + let state = self.state_at(hash)?; + PairsIter::new(state, prefix, start_key) + .map_err(|e| sp_blockchain::Error::from_state(Box::new(e))) + } + + fn storage( + &self, + hash: Block::Hash, + key: &StorageKey, + ) -> sp_blockchain::Result> { + Ok(self + .state_at(hash)? + .storage(&key.0) + .map_err(|e| sp_blockchain::Error::from_state(Box::new(e)))? + .map(StorageData)) + } + + fn storage_hash( + &self, + hash: ::Hash, + key: &StorageKey, + ) -> sp_blockchain::Result> { + self.state_at(hash)? + .storage_hash(&key.0) + .map_err(|e| sp_blockchain::Error::from_state(Box::new(e))) + } + + fn child_storage( + &self, + hash: ::Hash, + child_info: &ChildInfo, + key: &StorageKey, + ) -> sp_blockchain::Result> { + Ok(self + .state_at(hash)? + .child_storage(child_info, &key.0) + .map_err(|e| sp_blockchain::Error::from_state(Box::new(e)))? + .map(StorageData)) + } + + fn child_storage_hash( + &self, + hash: ::Hash, + child_info: &ChildInfo, + key: &StorageKey, + ) -> sp_blockchain::Result> { + self.state_at(hash)? + .child_storage_hash(child_info, &key.0) + .map_err(|e| sp_blockchain::Error::from_state(Box::new(e))) + } + + fn closest_merkle_value( + &self, + hash: ::Hash, + key: &StorageKey, + ) -> blockchain::Result::Hash>>> { + self.state_at(hash)? + .closest_merkle_value(&key.0) + .map_err(|e| sp_blockchain::Error::from_state(Box::new(e))) + } + + fn child_closest_merkle_value( + &self, + hash: ::Hash, + child_info: &ChildInfo, + key: &StorageKey, + ) -> blockchain::Result::Hash>>> { + self.state_at(hash)? + .child_closest_merkle_value(child_info, &key.0) + .map_err(|e| sp_blockchain::Error::from_state(Box::new(e))) + } +} + +impl HeaderMetadata for Client +where + B: backend::Backend, + E: CallExecutor, + Block: BlockT, +{ + type Error = sp_blockchain::Error; + + fn header_metadata( + &self, + hash: Block::Hash, + ) -> Result, Self::Error> { + self.backend.blockchain().header_metadata(hash) + } + + fn insert_header_metadata(&self, hash: Block::Hash, metadata: CachedHeaderMetadata) { + self.backend + .blockchain() + .insert_header_metadata(hash, metadata) + } + + fn remove_header_metadata(&self, hash: Block::Hash) { + self.backend.blockchain().remove_header_metadata(hash) + } +} + +impl ProvideUncles for Client +where + B: backend::Backend, + E: CallExecutor, + Block: BlockT, +{ + fn uncles( + &self, + target_hash: Block::Hash, + max_generation: NumberFor, + ) -> sp_blockchain::Result> { + Ok(Client::uncles(self, target_hash, max_generation)? + .into_iter() + .filter_map(|hash| Client::header(self, hash).unwrap_or(None)) + .collect()) + } +} + +impl ChainHeaderBackend for Client +where + B: backend::Backend, + E: CallExecutor + Send + Sync, + Block: BlockT, + RA: Send + Sync, +{ + fn header(&self, hash: Block::Hash) -> sp_blockchain::Result> { + self.backend.blockchain().header(hash) + } + + fn info(&self) -> blockchain::Info { + self.backend.blockchain().info() + } + + fn status(&self, hash: Block::Hash) -> sp_blockchain::Result { + self.backend.blockchain().status(hash) + } + + fn number( + &self, + hash: Block::Hash, + ) -> sp_blockchain::Result::Header as HeaderT>::Number>> { + self.backend.blockchain().number(hash) + } + + fn hash(&self, number: NumberFor) -> sp_blockchain::Result> { + self.backend.blockchain().hash(number) + } +} + +impl BlockIdTo for Client +where + B: backend::Backend, + E: CallExecutor + Send + Sync, + Block: BlockT, + RA: Send + Sync, +{ + type Error = Error; + + fn to_hash(&self, block_id: &BlockId) -> sp_blockchain::Result> { + self.block_hash_from_id(block_id) + } + + fn to_number( + &self, + block_id: &BlockId, + ) -> sp_blockchain::Result>> { + self.block_number_from_id(block_id) + } +} + +impl ChainHeaderBackend for &Client +where + B: backend::Backend, + E: CallExecutor + Send + Sync, + Block: BlockT, + RA: Send + Sync, +{ + fn header(&self, hash: Block::Hash) -> sp_blockchain::Result> { + self.backend.blockchain().header(hash) + } + + fn info(&self) -> blockchain::Info { + self.backend.blockchain().info() + } + + fn status(&self, hash: Block::Hash) -> sp_blockchain::Result { + (**self).status(hash) + } + + fn number( + &self, + hash: Block::Hash, + ) -> sp_blockchain::Result::Header as HeaderT>::Number>> { + (**self).number(hash) + } + + fn hash(&self, number: NumberFor) -> sp_blockchain::Result> { + (**self).hash(number) + } +} + +impl ProvideRuntimeApi for Client +where + B: backend::Backend, + E: CallExecutor + Send + Sync, + Block: BlockT, + RA: ConstructRuntimeApi + Send + Sync, +{ + type Api = >::RuntimeApi; + + fn runtime_api(&self) -> ApiRef<'_, Self::Api> { + RA::construct_runtime_api(self) + } +} + +impl CallApiAt for Client +where + B: backend::Backend, + E: CallExecutor + Send + Sync, + Block: BlockT, + RA: Send + Sync, +{ + type StateBackend = B::State; + + fn call_api_at(&self, params: CallApiAtParams) -> Result, sp_api::ApiError> { + self.executor + .contextual_call( + params.at, + params.function, + ¶ms.arguments, + params.overlayed_changes, + params.recorder, + params.call_context, + params.extensions, + ) + .map_err(Into::into) + } + + fn runtime_version_at(&self, hash: Block::Hash) -> Result { + CallExecutor::runtime_version(&self.executor, hash).map_err(Into::into) + } + + fn state_at(&self, at: Block::Hash) -> Result { + self.state_at(at).map_err(Into::into) + } + + fn initialize_extensions( + &self, + at: Block::Hash, + extensions: &mut sp_externalities::Extensions, + ) -> Result<(), sp_api::ApiError> { + let block_number = self.expect_block_number_from_id(&BlockId::Hash(at))?; + + extensions.merge( + self.executor + .execution_extensions() + .extensions(at, block_number), + ); + + Ok(()) + } +} + +/// NOTE: only use this implementation when you are sure there are NO consensus-level BlockImport +/// objects. Otherwise, importing blocks directly into the client would be bypassing +/// important verification work. +#[async_trait::async_trait] +impl sc_consensus::BlockImport for &Client +where + B: backend::Backend, + E: CallExecutor + Send + Sync, + Block: BlockT, + Client: ProvideRuntimeApi, + as ProvideRuntimeApi>::Api: CoreApi + ApiExt, + RA: Sync + Send, +{ + type Error = ConsensusError; + + /// Import a checked and validated block. + /// + /// NOTE: only use this implementation when there are NO consensus-level BlockImport + /// objects. Otherwise, importing blocks directly into the client would be bypassing + /// important verification work. + /// + /// If you are not sure that there are no BlockImport objects provided by the consensus + /// algorithm, don't use this function. + async fn import_block( + &self, + mut import_block: BlockImportParams, + ) -> Result { + let span = tracing::span!(tracing::Level::DEBUG, "import_block"); + let _enter = span.enter(); + + let storage_changes = match self + .prepare_block_storage_changes(&mut import_block) + .map_err(|e| { + warn!("Block prepare storage changes error: {}", e); + ConsensusError::ClientImport(e.to_string()) + })? { + PrepareStorageChangesResult::Discard(res) => return Ok(res), + PrepareStorageChangesResult::Import(storage_changes) => storage_changes, + }; + + self.lock_import_and_run(|operation| { + self.apply_block(operation, import_block, storage_changes) + }) + .map_err(|e| { + warn!("Block import error: {}", e); + ConsensusError::ClientImport(e.to_string()) + }) + } + + /// Check block preconditions. + async fn check_block( + &self, + block: BlockCheckParams, + ) -> Result { + let BlockCheckParams { + hash, + number, + parent_hash, + allow_missing_state, + import_existing, + allow_missing_parent, + } = block; + + // Check the block against white and black lists if any are defined + // (i.e. fork blocks and bad blocks respectively) + match self.block_rules.lookup(number, &hash) { + BlockLookupResult::KnownBad => { + trace!("Rejecting known bad block: #{} {:?}", number, hash); + return Ok(ImportResult::KnownBad); + } + BlockLookupResult::Expected(expected_hash) => { + trace!( + "Rejecting block from known invalid fork. Got {:?}, expected: {:?} at height {}", + hash, + expected_hash, + number + ); + return Ok(ImportResult::KnownBad); + } + BlockLookupResult::NotSpecial => {} + } + + // Own status must be checked first. If the block and ancestry is pruned + // this function must return `AlreadyInChain` rather than `MissingState` + match self + .block_status(hash) + .map_err(|e| ConsensusError::ClientImport(e.to_string()))? + { + BlockStatus::InChainWithState | BlockStatus::Queued => { + return Ok(ImportResult::AlreadyInChain) + } + BlockStatus::InChainPruned if !import_existing => { + return Ok(ImportResult::AlreadyInChain) + } + BlockStatus::InChainPruned => {} + BlockStatus::Unknown => {} + BlockStatus::KnownBad => return Ok(ImportResult::KnownBad), + } + + match self + .block_status(parent_hash) + .map_err(|e| ConsensusError::ClientImport(e.to_string()))? + { + BlockStatus::InChainWithState | BlockStatus::Queued => {} + BlockStatus::Unknown if allow_missing_parent => {} + BlockStatus::Unknown => return Ok(ImportResult::UnknownParent), + BlockStatus::InChainPruned if allow_missing_state => {} + BlockStatus::InChainPruned => return Ok(ImportResult::MissingState), + BlockStatus::KnownBad => return Ok(ImportResult::KnownBad), + } + + Ok(ImportResult::imported(false)) + } +} + +#[async_trait::async_trait] +impl sc_consensus::BlockImport for Client +where + B: backend::Backend, + E: CallExecutor + Send + Sync, + Block: BlockT, + Self: ProvideRuntimeApi, + >::Api: CoreApi + ApiExt, + RA: Sync + Send, +{ + type Error = ConsensusError; + + async fn check_block( + &self, + block: BlockCheckParams, + ) -> Result { + (&self).check_block(block).await + } + + async fn import_block( + &self, + import_block: BlockImportParams, + ) -> Result { + (&self).import_block(import_block).await + } +} + +impl Finalizer for Client +where + B: backend::Backend, + E: CallExecutor, + Block: BlockT, +{ + fn apply_finality( + &self, + operation: &mut ClientImportOperation, + hash: Block::Hash, + justification: Option, + notify: bool, + ) -> sp_blockchain::Result<()> { + let info = self.backend.blockchain().info(); + self.apply_finality_with_block_hash(operation, hash, justification, &info, notify) + } + + fn finalize_block( + &self, + hash: Block::Hash, + justification: Option, + notify: bool, + ) -> sp_blockchain::Result<()> { + self.lock_import_and_run(|operation| { + self.apply_finality(operation, hash, justification, notify) + }) + } +} + +impl Finalizer for &Client +where + B: backend::Backend, + E: CallExecutor, + Block: BlockT, +{ + fn apply_finality( + &self, + operation: &mut ClientImportOperation, + hash: Block::Hash, + justification: Option, + notify: bool, + ) -> sp_blockchain::Result<()> { + (**self).apply_finality(operation, hash, justification, notify) + } + + fn finalize_block( + &self, + hash: Block::Hash, + justification: Option, + notify: bool, + ) -> sp_blockchain::Result<()> { + (**self).finalize_block(hash, justification, notify) + } +} + +impl PreCommitActions for Client +where + Block: BlockT, +{ + fn register_import_action(&self, action: OnImportAction) { + self.import_actions.lock().push(action); + } + + fn register_finality_action(&self, action: OnFinalityAction) { + self.finality_actions.lock().push(action); + } +} + +impl BlockchainEvents for Client +where + E: CallExecutor, + Block: BlockT, +{ + /// Get block import event stream. + fn import_notification_stream(&self) -> ImportNotifications { + let (sink, stream) = tracing_unbounded("mpsc_import_notification_stream", 100_000); + self.import_notification_sinks.lock().push(sink); + stream + } + + fn every_import_notification_stream(&self) -> ImportNotifications { + let (sink, stream) = tracing_unbounded("mpsc_every_import_notification_stream", 100_000); + self.every_import_notification_sinks.lock().push(sink); + stream + } + + fn finality_notification_stream(&self) -> FinalityNotifications { + let (sink, stream) = tracing_unbounded("mpsc_finality_notification_stream", 100_000); + self.finality_notification_sinks.lock().push(sink); + stream + } + + /// Get storage changes event stream. + fn storage_changes_notification_stream( + &self, + filter_keys: Option<&[StorageKey]>, + child_filter_keys: Option<&[(StorageKey, Option>)]>, + ) -> sp_blockchain::Result> { + Ok(self + .storage_notifications + .listen(filter_keys, child_filter_keys)) + } +} + +impl BlockBackend for Client +where + B: backend::Backend, + E: CallExecutor, + Block: BlockT, +{ + fn block_body( + &self, + hash: Block::Hash, + ) -> sp_blockchain::Result::Extrinsic>>> { + self.body(hash) + } + + fn block(&self, hash: Block::Hash) -> sp_blockchain::Result>> { + Ok( + match ( + self.header(hash)?, + self.body(hash)?, + self.justifications(hash)?, + ) { + (Some(header), Some(extrinsics), justifications) => Some(SignedBlock { + block: Block::new(header, extrinsics), + justifications, + }), + _ => None, + }, + ) + } + + fn block_status(&self, hash: Block::Hash) -> sp_blockchain::Result { + Client::block_status(self, hash) + } + + fn justifications(&self, hash: Block::Hash) -> sp_blockchain::Result> { + self.backend.blockchain().justifications(hash) + } + + fn block_hash(&self, number: NumberFor) -> sp_blockchain::Result> { + self.backend.blockchain().hash(number) + } + + fn indexed_transaction(&self, hash: Block::Hash) -> sp_blockchain::Result>> { + self.backend.blockchain().indexed_transaction(hash) + } + + fn has_indexed_transaction(&self, hash: Block::Hash) -> sp_blockchain::Result { + self.backend.blockchain().has_indexed_transaction(hash) + } + + fn block_indexed_body(&self, hash: Block::Hash) -> sp_blockchain::Result>>> { + self.backend.blockchain().block_indexed_body(hash) + } + + fn requires_full_sync(&self) -> bool { + self.backend.requires_full_sync() + } +} + +impl backend::AuxStore for Client +where + B: backend::Backend, + E: CallExecutor, + Block: BlockT, + Self: ProvideRuntimeApi, + >::Api: CoreApi, +{ + /// Insert auxiliary data into key-value store. + fn insert_aux< + 'a, + 'b: 'a, + 'c: 'a, + I: IntoIterator, + D: IntoIterator, + >( + &self, + insert: I, + delete: D, + ) -> sp_blockchain::Result<()> { + // Import is locked here because we may have other block import + // operations that tries to set aux data. Note that for consensus + // layer, one can always use atomic operations to make sure + // import is only locked once. + self.lock_import_and_run(|operation| apply_aux(operation, insert, delete)) + } + /// Query auxiliary data from key-value store. + fn get_aux(&self, key: &[u8]) -> sp_blockchain::Result>> { + backend::AuxStore::get_aux(&*self.backend, key) + } +} + +impl backend::AuxStore for &Client +where + B: backend::Backend, + E: CallExecutor, + Block: BlockT, + Client: ProvideRuntimeApi, + as ProvideRuntimeApi>::Api: CoreApi, +{ + fn insert_aux< + 'a, + 'b: 'a, + 'c: 'a, + I: IntoIterator, + D: IntoIterator, + >( + &self, + insert: I, + delete: D, + ) -> sp_blockchain::Result<()> { + (**self).insert_aux(insert, delete) + } + + fn get_aux(&self, key: &[u8]) -> sp_blockchain::Result>> { + (**self).get_aux(key) + } +} + +impl sp_consensus::block_validation::Chain for Client +where + BE: backend::Backend, + E: CallExecutor, + B: BlockT, +{ + fn block_status( + &self, + hash: B::Hash, + ) -> Result> { + Client::block_status(self, hash).map_err(|e| Box::new(e) as Box<_>) + } +} + +impl sp_transaction_storage_proof::IndexedBody for Client +where + BE: backend::Backend, + E: CallExecutor, + B: BlockT, +{ + fn block_indexed_body( + &self, + number: NumberFor, + ) -> Result>>, sp_transaction_storage_proof::Error> { + let hash = match self + .backend + .blockchain() + .block_hash_from_id(&BlockId::Number(number)) + .map_err(|e| sp_transaction_storage_proof::Error::Application(Box::new(e)))? + { + Some(hash) => hash, + None => return Ok(None), + }; + + self.backend + .blockchain() + .block_indexed_body(hash) + .map_err(|e| sp_transaction_storage_proof::Error::Application(Box::new(e))) + } + + fn number( + &self, + hash: B::Hash, + ) -> Result>, sp_transaction_storage_proof::Error> { + self.backend + .blockchain() + .number(hash) + .map_err(|e| sp_transaction_storage_proof::Error::Application(Box::new(e))) + } +} diff --git a/substrate/service/src/client/code_provider.rs b/substrate/service/src/client/code_provider.rs new file mode 100644 index 00000000000..e91c4f4e7c8 --- /dev/null +++ b/substrate/service/src/client/code_provider.rs @@ -0,0 +1,351 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use super::{client::ClientConfig, wasm_override::WasmOverride, wasm_substitutes::WasmSubstitutes}; +use sc_client_api::backend; +use sc_executor::{RuntimeVersion, RuntimeVersionOf}; +use sp_core::traits::{FetchRuntimeCode, RuntimeCode}; +use sp_runtime::traits::Block as BlockT; +use sp_state_machine::{Ext, OverlayedChanges}; +use std::sync::Arc; + +/// Provider for fetching `:code` of a block. +/// +/// As a node can run with code overrides or substitutes, this will ensure that these are taken into +/// account before returning the actual `code` for a block. +pub struct CodeProvider { + backend: Arc, + executor: Arc, + wasm_override: Arc>, + wasm_substitutes: WasmSubstitutes, +} + +impl Clone for CodeProvider { + fn clone(&self) -> Self { + Self { + backend: self.backend.clone(), + executor: self.executor.clone(), + wasm_override: self.wasm_override.clone(), + wasm_substitutes: self.wasm_substitutes.clone(), + } + } +} + +impl CodeProvider +where + Block: BlockT, + Backend: backend::Backend, + Executor: RuntimeVersionOf, +{ + /// Create a new instance. + pub fn new( + client_config: &ClientConfig, + executor: Executor, + backend: Arc, + ) -> sp_blockchain::Result { + let wasm_override = client_config + .wasm_runtime_overrides + .as_ref() + .map(|p| WasmOverride::new(p.clone(), &executor)) + .transpose()?; + + let executor = Arc::new(executor); + + let wasm_substitutes = WasmSubstitutes::new( + client_config.wasm_runtime_substitutes.clone(), + executor.clone(), + backend.clone(), + )?; + + Ok(Self { + backend, + executor, + wasm_override: Arc::new(wasm_override), + wasm_substitutes, + }) + } + + /// Returns the `:code` for the given `block`. + /// + /// This takes into account potential overrides/substitutes. + pub fn code_at_ignoring_overrides(&self, block: Block::Hash) -> sp_blockchain::Result> { + let state = self.backend.state_at(block)?; + + let state_runtime_code = sp_state_machine::backend::BackendRuntimeCode::new(&state); + let runtime_code = state_runtime_code + .runtime_code() + .map_err(sp_blockchain::Error::RuntimeCode)?; + + self.maybe_override_code_internal(runtime_code, &state, block, true) + .and_then(|r| { + r.0.fetch_runtime_code().map(Into::into).ok_or_else(|| { + sp_blockchain::Error::Backend("Could not find `:code` in backend.".into()) + }) + }) + } + + /// Maybe override the given `onchain_code`. + /// + /// This takes into account potential overrides/substitutes. + pub fn maybe_override_code<'a>( + &'a self, + onchain_code: RuntimeCode<'a>, + state: &Backend::State, + hash: Block::Hash, + ) -> sp_blockchain::Result<(RuntimeCode<'a>, RuntimeVersion)> { + self.maybe_override_code_internal(onchain_code, state, hash, false) + } + + /// Maybe override the given `onchain_code`. + /// + /// This takes into account potential overrides(depending on `ignore_overrides`)/substitutes. + fn maybe_override_code_internal<'a>( + &'a self, + onchain_code: RuntimeCode<'a>, + state: &Backend::State, + hash: Block::Hash, + ignore_overrides: bool, + ) -> sp_blockchain::Result<(RuntimeCode<'a>, RuntimeVersion)> { + let on_chain_version = self.on_chain_runtime_version(&onchain_code, state)?; + let code_and_version = if let Some(d) = self.wasm_override.as_ref().as_ref().and_then(|o| { + if ignore_overrides { + return None; + } + + o.get( + &on_chain_version.spec_version, + onchain_code.heap_pages, + &on_chain_version.spec_name, + ) + }) { + tracing::debug!(target: "code-provider::overrides", block = ?hash, "using WASM override"); + d + } else if let Some(s) = + self.wasm_substitutes + .get(on_chain_version.spec_version, onchain_code.heap_pages, hash) + { + tracing::debug!(target: "code-provider::substitutes", block = ?hash, "Using WASM substitute"); + s + } else { + tracing::debug!( + target: "code-provider", + block = ?hash, + "Neither WASM override nor substitute available, using onchain code", + ); + (onchain_code, on_chain_version) + }; + + Ok(code_and_version) + } + + /// Returns the on chain runtime version. + fn on_chain_runtime_version( + &self, + code: &RuntimeCode, + state: &Backend::State, + ) -> sp_blockchain::Result { + let mut overlay = OverlayedChanges::default(); + + let mut ext = Ext::new(&mut overlay, state, None); + + self.executor + .runtime_version(&mut ext, code) + .map_err(|e| sp_blockchain::Error::VersionInvalid(e.to_string())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use backend::Backend; + use sc_client_api::{in_mem, HeaderBackend}; + use sc_executor::WasmExecutor; + use sp_core::{ + testing::TaskExecutor, + traits::{FetchRuntimeCode, WrappedRuntimeCode}, + }; + use std::collections::HashMap; + use substrate_test_runtime_client::{runtime, GenesisInit}; + + #[test] + fn no_override_no_substitutes_work() { + let executor = WasmExecutor::default(); + + let code_fetcher = WrappedRuntimeCode(substrate_test_runtime::wasm_binary_unwrap().into()); + let onchain_code = RuntimeCode { + code_fetcher: &code_fetcher, + heap_pages: Some(128), + hash: vec![0, 0, 0, 0], + }; + + let backend = Arc::new(in_mem::Backend::::new()); + + // wasm_runtime_overrides is `None` here because we construct the + // LocalCallExecutor directly later on + let client_config = ClientConfig::default(); + + let genesis_block_builder = crate::GenesisBlockBuilder::new( + &substrate_test_runtime_client::GenesisParameters::default().genesis_storage(), + !client_config.no_genesis, + backend.clone(), + executor.clone(), + ) + .expect("Creates genesis block builder"); + + // client is used for the convenience of creating and inserting the genesis block. + let _client = + crate::client::new_with_backend::<_, _, runtime::Block, _, runtime::RuntimeApi>( + backend.clone(), + executor.clone(), + genesis_block_builder, + Box::new(TaskExecutor::new()), + None, + None, + client_config.clone(), + ) + .expect("Creates a client"); + + let executor = Arc::new(executor); + + let code_provider = CodeProvider { + backend: backend.clone(), + executor: executor.clone(), + wasm_override: Arc::new(None), + wasm_substitutes: WasmSubstitutes::new(Default::default(), executor, backend.clone()) + .unwrap(), + }; + + let check = code_provider + .maybe_override_code( + onchain_code, + &backend + .state_at(backend.blockchain().info().genesis_hash) + .unwrap(), + backend.blockchain().info().genesis_hash, + ) + .expect("RuntimeCode override") + .0; + + assert_eq!( + code_fetcher.fetch_runtime_code(), + check.fetch_runtime_code() + ); + } + + #[test] + fn should_get_override_if_exists() { + let executor = WasmExecutor::default(); + + let overrides = crate::client::wasm_override::dummy_overrides(); + let onchain_code = WrappedRuntimeCode(substrate_test_runtime::wasm_binary_unwrap().into()); + let onchain_code = RuntimeCode { + code_fetcher: &onchain_code, + heap_pages: Some(128), + hash: vec![0, 0, 0, 0], + }; + + let backend = Arc::new(in_mem::Backend::::new()); + + // wasm_runtime_overrides is `None` here because we construct the + // LocalCallExecutor directly later on + let client_config = ClientConfig::default(); + + let genesis_block_builder = crate::GenesisBlockBuilder::new( + &substrate_test_runtime_client::GenesisParameters::default().genesis_storage(), + !client_config.no_genesis, + backend.clone(), + executor.clone(), + ) + .expect("Creates genesis block builder"); + + // client is used for the convenience of creating and inserting the genesis block. + let _client = + crate::client::new_with_backend::<_, _, runtime::Block, _, runtime::RuntimeApi>( + backend.clone(), + executor.clone(), + genesis_block_builder, + Box::new(TaskExecutor::new()), + None, + None, + client_config.clone(), + ) + .expect("Creates a client"); + + let executor = Arc::new(executor); + + let code_provider = CodeProvider { + backend: backend.clone(), + executor: executor.clone(), + wasm_override: Arc::new(Some(overrides)), + wasm_substitutes: WasmSubstitutes::new(Default::default(), executor, backend.clone()) + .unwrap(), + }; + + let check = code_provider + .maybe_override_code( + onchain_code, + &backend + .state_at(backend.blockchain().info().genesis_hash) + .unwrap(), + backend.blockchain().info().genesis_hash, + ) + .expect("RuntimeCode override") + .0; + + assert_eq!( + Some(vec![2, 2, 2, 2, 2, 2, 2, 2]), + check.fetch_runtime_code().map(Into::into) + ); + } + + #[test] + fn returns_runtime_version_from_substitute() { + const SUBSTITUTE_SPEC_NAME: &str = "substitute-spec-name-cool"; + + let executor = WasmExecutor::default(); + + let backend = Arc::new(in_mem::Backend::::new()); + + // Let's only override the `spec_name` for our testing purposes. + let substitute = sp_version::embed::embed_runtime_version( + substrate_test_runtime::WASM_BINARY_BLOATY.unwrap(), + sp_version::RuntimeVersion { + spec_name: SUBSTITUTE_SPEC_NAME.into(), + ..substrate_test_runtime::VERSION + }, + ) + .unwrap(); + + let client_config = crate::client::ClientConfig { + wasm_runtime_substitutes: vec![(0, substitute)].into_iter().collect::>(), + ..Default::default() + }; + + let genesis_block_builder = crate::GenesisBlockBuilder::new( + &substrate_test_runtime_client::GenesisParameters::default().genesis_storage(), + !client_config.no_genesis, + backend.clone(), + executor.clone(), + ) + .expect("Creates genesis block builder"); + + // client is used for the convenience of creating and inserting the genesis block. + let client = + crate::client::new_with_backend::<_, _, runtime::Block, _, runtime::RuntimeApi>( + backend.clone(), + executor.clone(), + genesis_block_builder, + Box::new(TaskExecutor::new()), + None, + None, + client_config, + ) + .expect("Creates a client"); + + let version = client + .runtime_version_at(client.chain_info().genesis_hash) + .unwrap(); + + assert_eq!(SUBSTITUTE_SPEC_NAME, &*version.spec_name); + } +} diff --git a/substrate/service/src/client/mod.rs b/substrate/service/src/client/mod.rs new file mode 100644 index 00000000000..8739c25758d --- /dev/null +++ b/substrate/service/src/client/mod.rs @@ -0,0 +1,45 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Substrate Client and associated logic. +//! +//! The [`Client`] is one of the most important components of Substrate. It mainly comprises two +//! parts: +//! +//! - A database containing the blocks and chain state, generally referred to as +//! the [`Backend`](sc_client_api::backend::Backend). +//! - A runtime environment, generally referred to as the +//! [`Executor`](sc_client_api::call_executor::CallExecutor). +//! +//! # Initialization +//! +//! Creating a [`Client`] is done by calling the `new` method and passing to it a +//! [`Backend`](sc_client_api::backend::Backend) and an +//! [`Executor`](sc_client_api::call_executor::CallExecutor). +//! +//! The former is typically provided by the `sc-client-db` crate. +//! +//! The latter typically requires passing one of: +//! +//! - A [`LocalCallExecutor`] running the runtime locally. +//! - A `RemoteCallExecutor` that will ask a third-party to perform the executions. +//! - A `RemoteOrLocalCallExecutor` combination of the two. +//! +//! Additionally, the fourth generic parameter of the `Client` is a marker type representing +//! the ways in which the runtime can interface with the outside. Any code that builds a `Client` +//! is responsible for putting the right marker. + +mod block_rules; +mod call_executor; +mod client; +mod code_provider; +mod notification_pinning; +mod wasm_override; +mod wasm_substitutes; + +pub use call_executor::LocalCallExecutor; +pub use client::{Client, ClientConfig}; +pub(crate) use code_provider::CodeProvider; + +#[cfg(feature = "test-helpers")] +pub use self::client::{new_in_mem, new_with_backend}; diff --git a/substrate/service/src/client/notification_pinning.rs b/substrate/service/src/client/notification_pinning.rs new file mode 100644 index 00000000000..26dff36f0d5 --- /dev/null +++ b/substrate/service/src/client/notification_pinning.rs @@ -0,0 +1,351 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Notification pinning related logic. +//! +//! This file contains a worker that should be started when a new client instance is created. +//! The goal is to avoid pruning of blocks that have active notifications in the node. Every +//! recipient of notifications should receive the chance to act upon them. In addition, notification +//! listeners can hold onto a [`sc_client_api::UnpinHandle`] to keep a block pinned. Once the handle +//! is dropped, a message is sent and the worker unpins the respective block. +use std::{ + marker::PhantomData, + sync::{Arc, Weak}, +}; + +use futures::StreamExt; +use sc_client_api::{Backend, UnpinWorkerMessage}; + +use sc_utils::mpsc::TracingUnboundedReceiver; +use schnellru::Limiter; +use sp_runtime::traits::Block as BlockT; + +const LOG_TARGET: &str = "db::notification_pinning"; +const NOTIFICATION_PINNING_LIMIT: usize = 1024; + +/// A limiter which automatically unpins blocks that leave the data structure. +#[derive(Clone, Debug)] +struct UnpinningByLengthLimiter> { + max_length: usize, + backend: Weak, + _phantom: PhantomData, +} + +impl> UnpinningByLengthLimiter { + /// Creates a new length limiter with a given `max_length`. + pub fn new(max_length: usize, backend: Weak) -> UnpinningByLengthLimiter { + UnpinningByLengthLimiter { + max_length, + backend, + _phantom: PhantomData::, + } + } +} + +impl> Limiter + for UnpinningByLengthLimiter +{ + type KeyToInsert<'a> = Block::Hash; + type LinkType = usize; + + fn is_over_the_limit(&self, length: usize) -> bool { + length > self.max_length + } + + fn on_insert( + &mut self, + _length: usize, + key: Self::KeyToInsert<'_>, + value: u32, + ) -> Option<(Block::Hash, u32)> { + log::debug!(target: LOG_TARGET, "Pinning block based on notification. hash = {key}"); + if self.max_length > 0 { + Some((key, value)) + } else { + None + } + } + + fn on_replace( + &mut self, + _length: usize, + _old_key: &mut Block::Hash, + _new_key: Block::Hash, + _old_value: &mut u32, + _new_value: &mut u32, + ) -> bool { + true + } + + fn on_removed(&mut self, key: &mut Block::Hash, references: &mut u32) { + // If reference count was larger than 0 on removal, + // the item was removed due to capacity limitations. + // Since the cache should be large enough for pinned items, + // we want to know about these evictions. + if *references > 0 { + log::warn!( + target: LOG_TARGET, + "Notification block pinning limit reached. Unpinning block with hash = {key:?}" + ); + if let Some(backend) = self.backend.upgrade() { + (0..*references).for_each(|_| backend.unpin_block(*key)); + } + } else { + log::trace!( + target: LOG_TARGET, + "Unpinned block. hash = {key:?}", + ) + } + } + + fn on_cleared(&mut self) {} + + fn on_grow(&mut self, _new_memory_usage: usize) -> bool { + true + } +} + +/// Worker for the handling of notification pinning. +/// +/// It receives messages from a receiver and pins/unpins based on the incoming messages. +/// All notification related unpinning should go through this worker. If the maximum number of +/// notification pins is reached, the block from the oldest notification is unpinned. +pub struct NotificationPinningWorker> { + unpin_message_rx: TracingUnboundedReceiver>, + task_backend: Weak, + pinned_blocks: schnellru::LruMap>, +} + +impl> NotificationPinningWorker { + /// Creates a new `NotificationPinningWorker`. + pub fn new( + unpin_message_rx: TracingUnboundedReceiver>, + task_backend: Arc, + ) -> Self { + let pinned_blocks = + schnellru::LruMap::>::new( + UnpinningByLengthLimiter::new( + NOTIFICATION_PINNING_LIMIT, + Arc::downgrade(&task_backend), + ), + ); + Self { + unpin_message_rx, + task_backend: Arc::downgrade(&task_backend), + pinned_blocks, + } + } + + fn handle_announce_message(&mut self, hash: Block::Hash) { + if let Some(entry) = self.pinned_blocks.get_or_insert(hash, Default::default) { + *entry += 1; + } + } + + fn handle_unpin_message(&mut self, hash: Block::Hash) -> Result<(), ()> { + if let Some(refcount) = self.pinned_blocks.peek_mut(&hash) { + *refcount -= 1; + if *refcount == 0 { + self.pinned_blocks.remove(&hash); + } + if let Some(backend) = self.task_backend.upgrade() { + log::debug!(target: LOG_TARGET, "Reducing pinning refcount for block hash = {hash:?}"); + backend.unpin_block(hash); + } else { + log::debug!(target: LOG_TARGET, "Terminating unpin-worker, backend reference was dropped."); + return Err(()); + } + } else { + log::debug!(target: LOG_TARGET, "Received unpin message for already unpinned block. hash = {hash:?}"); + } + Ok(()) + } + + /// Start working on the received messages. + /// + /// The worker maintains a map which keeps track of the pinned blocks and their reference count. + /// Depending upon the received message, it acts to pin/unpin the block. + pub async fn run(mut self) { + while let Some(message) = self.unpin_message_rx.next().await { + match message { + UnpinWorkerMessage::AnnouncePin(hash) => self.handle_announce_message(hash), + UnpinWorkerMessage::Unpin(hash) => { + if self.handle_unpin_message(hash).is_err() { + return; + } + } + } + } + log::debug!(target: LOG_TARGET, "Terminating unpin-worker, stream terminated.") + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use sc_client_api::{Backend, UnpinWorkerMessage}; + use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver}; + use sp_core::H256; + use sp_runtime::traits::Block as BlockT; + + type Block = substrate_test_runtime_client::runtime::Block; + + use super::{NotificationPinningWorker, UnpinningByLengthLimiter}; + + impl> NotificationPinningWorker { + fn new_with_limit( + unpin_message_rx: TracingUnboundedReceiver>, + task_backend: Arc, + limit: usize, + ) -> Self { + let pinned_blocks = + schnellru::LruMap::>::new( + UnpinningByLengthLimiter::new(limit, Arc::downgrade(&task_backend)), + ); + Self { + unpin_message_rx, + task_backend: Arc::downgrade(&task_backend), + pinned_blocks, + } + } + + fn lru( + &self, + ) -> &schnellru::LruMap> { + &self.pinned_blocks + } + } + + #[test] + fn pinning_worker_handles_base_case() { + let (_tx, rx) = tracing_unbounded("testing", 1000); + + let backend = Arc::new(sc_client_api::in_mem::Backend::::new()); + + let hash = H256::random(); + + let mut worker = NotificationPinningWorker::new(rx, backend.clone()); + + // Block got pinned and unpin message should unpin in the backend. + let _ = backend.pin_block(hash); + assert_eq!(backend.pin_refs(&hash), Some(1)); + + worker.handle_announce_message(hash); + assert_eq!(worker.lru().len(), 1); + + let _ = worker.handle_unpin_message(hash); + + assert_eq!(backend.pin_refs(&hash), Some(0)); + assert!(worker.lru().is_empty()); + } + + #[test] + fn pinning_worker_handles_multiple_pins() { + let (_tx, rx) = tracing_unbounded("testing", 1000); + + let backend = Arc::new(sc_client_api::in_mem::Backend::::new()); + + let hash = H256::random(); + + let mut worker = NotificationPinningWorker::new(rx, backend.clone()); + // Block got pinned multiple times. + let _ = backend.pin_block(hash); + let _ = backend.pin_block(hash); + let _ = backend.pin_block(hash); + assert_eq!(backend.pin_refs(&hash), Some(3)); + + worker.handle_announce_message(hash); + worker.handle_announce_message(hash); + worker.handle_announce_message(hash); + assert_eq!(worker.lru().len(), 1); + + let _ = worker.handle_unpin_message(hash); + assert_eq!(backend.pin_refs(&hash), Some(2)); + let _ = worker.handle_unpin_message(hash); + assert_eq!(backend.pin_refs(&hash), Some(1)); + let _ = worker.handle_unpin_message(hash); + assert_eq!(backend.pin_refs(&hash), Some(0)); + assert!(worker.lru().is_empty()); + + let _ = worker.handle_unpin_message(hash); + assert_eq!(backend.pin_refs(&hash), Some(0)); + } + + #[test] + fn pinning_worker_handles_too_many_unpins() { + let (_tx, rx) = tracing_unbounded("testing", 1000); + + let backend = Arc::new(sc_client_api::in_mem::Backend::::new()); + + let hash = H256::random(); + let hash2 = H256::random(); + + let mut worker = NotificationPinningWorker::new(rx, backend.clone()); + // Block was announced once but unpinned multiple times. The worker should ignore the + // additional unpins. + let _ = backend.pin_block(hash); + let _ = backend.pin_block(hash); + let _ = backend.pin_block(hash); + assert_eq!(backend.pin_refs(&hash), Some(3)); + + worker.handle_announce_message(hash); + assert_eq!(worker.lru().len(), 1); + + let _ = worker.handle_unpin_message(hash); + assert_eq!(backend.pin_refs(&hash), Some(2)); + let _ = worker.handle_unpin_message(hash); + assert_eq!(backend.pin_refs(&hash), Some(2)); + assert!(worker.lru().is_empty()); + + let _ = worker.handle_unpin_message(hash2); + assert!(worker.lru().is_empty()); + assert_eq!(backend.pin_refs(&hash2), None); + } + + #[test] + fn pinning_worker_should_evict_when_limit_reached() { + let (_tx, rx) = tracing_unbounded("testing", 1000); + + let backend = Arc::new(sc_client_api::in_mem::Backend::::new()); + + let hash1 = H256::random(); + let hash2 = H256::random(); + let hash3 = H256::random(); + let hash4 = H256::random(); + + // Only two items fit into the cache. + let mut worker = NotificationPinningWorker::new_with_limit(rx, backend.clone(), 2); + + // Multiple blocks are announced but the cache size is too small. We expect that blocks + // are evicted by the cache and unpinned in the backend. + let _ = backend.pin_block(hash1); + let _ = backend.pin_block(hash2); + let _ = backend.pin_block(hash3); + assert_eq!(backend.pin_refs(&hash1), Some(1)); + assert_eq!(backend.pin_refs(&hash2), Some(1)); + assert_eq!(backend.pin_refs(&hash3), Some(1)); + + worker.handle_announce_message(hash1); + assert!(worker.lru().peek(&hash1).is_some()); + worker.handle_announce_message(hash2); + assert!(worker.lru().peek(&hash2).is_some()); + worker.handle_announce_message(hash3); + assert!(worker.lru().peek(&hash3).is_some()); + assert!(worker.lru().peek(&hash2).is_some()); + assert_eq!(worker.lru().len(), 2); + + // Hash 1 should have gotten unpinned, since its oldest. + assert_eq!(backend.pin_refs(&hash1), Some(0)); + assert_eq!(backend.pin_refs(&hash2), Some(1)); + assert_eq!(backend.pin_refs(&hash3), Some(1)); + + // Hash 2 is getting bumped. + worker.handle_announce_message(hash2); + assert_eq!(worker.lru().peek(&hash2), Some(&2)); + + // Since hash 2 was accessed, evict hash 3. + worker.handle_announce_message(hash4); + assert_eq!(worker.lru().peek(&hash3), None); + } +} diff --git a/substrate/service/src/client/wasm_override.rs b/substrate/service/src/client/wasm_override.rs new file mode 100644 index 00000000000..5a1bd23ca20 --- /dev/null +++ b/substrate/service/src/client/wasm_override.rs @@ -0,0 +1,365 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! # WASM Local Blob-Override +//! +//! WASM Local blob override provides tools to replace on-chain WASM with custom WASM. +//! These customized WASM blobs may include functionality that is not included in the +//! on-chain WASM, such as tracing or debugging information. This extra information is especially +//! useful in external scenarios, like exchanges or archive nodes. +//! +//! ## Usage +//! +//! WASM overrides may be enabled with the `--wasm-runtime-overrides` argument. The argument +//! expects a path to a directory that holds custom WASM. +//! +//! Any file ending in '.wasm' will be scraped and instantiated as a WASM blob. WASM can be built by +//! compiling the required runtime with the changes needed. For example, compiling a runtime with +//! tracing enabled would produce a WASM blob that can used. +//! +//! A custom WASM blob will override on-chain WASM if the spec version matches. If it is +//! required to overrides multiple runtimes, multiple WASM blobs matching each of the spec versions +//! needed must be provided in the given directory. + +use sc_executor::RuntimeVersionOf; +use sp_blockchain::Result; +use sp_core::traits::{FetchRuntimeCode, RuntimeCode, WrappedRuntimeCode}; +use sp_state_machine::BasicExternalities; +use sp_version::RuntimeVersion; +use std::{ + collections::{hash_map::DefaultHasher, HashMap}, + fs, + hash::Hasher as _, + path::{Path, PathBuf}, + time::{Duration, Instant}, +}; + +/// The interval in that we will print a warning when a wasm blob `spec_name` +/// doesn't match with the on-chain `spec_name`. +const WARN_INTERVAL: Duration = Duration::from_secs(30); + +/// Auxiliary structure that holds a wasm blob and its hash. +#[derive(Debug)] +struct WasmBlob { + /// The actual wasm blob, aka the code. + code: Vec, + /// The hash of [`Self::code`]. + hash: Vec, + /// The path where this blob was found. + path: PathBuf, + /// The runtime version of this blob. + version: RuntimeVersion, + /// When was the last time we have warned about the wasm blob having + /// a wrong `spec_name`? + last_warn: parking_lot::Mutex>, +} + +impl WasmBlob { + fn new(code: Vec, hash: Vec, path: PathBuf, version: RuntimeVersion) -> Self { + Self { + code, + hash, + path, + version, + last_warn: Default::default(), + } + } + + fn runtime_code(&self, heap_pages: Option) -> RuntimeCode<'_> { + RuntimeCode { + code_fetcher: self, + hash: self.hash.clone(), + heap_pages, + } + } +} + +/// Make a hash out of a byte string using the default rust hasher +fn make_hash(val: &K) -> Vec { + let mut state = DefaultHasher::new(); + val.hash(&mut state); + state.finish().to_le_bytes().to_vec() +} + +impl FetchRuntimeCode for WasmBlob { + fn fetch_runtime_code(&self) -> Option> { + Some(self.code.as_slice().into()) + } +} + +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum WasmOverrideError { + #[error("Failed to get runtime version: {0}")] + VersionInvalid(String), + + #[error("WASM override IO error")] + Io(PathBuf, #[source] std::io::Error), + + #[error("Overwriting WASM requires a directory where local \ + WASM is stored. {} is not a directory", .0.display())] + NotADirectory(PathBuf), + + #[error("Duplicate WASM Runtimes found: \n{}\n", .0.join("\n") )] + DuplicateRuntime(Vec), +} + +impl From for sp_blockchain::Error { + fn from(err: WasmOverrideError) -> Self { + Self::Application(Box::new(err)) + } +} + +/// Scrapes WASM from a folder and returns WASM from that folder +/// if the runtime spec version matches. +#[derive(Debug)] +pub struct WasmOverride { + // Map of runtime spec version -> Wasm Blob + overrides: HashMap, +} + +impl WasmOverride { + pub fn new(path: P, executor: &E) -> Result + where + P: AsRef, + E: RuntimeVersionOf, + { + let overrides = Self::scrape_overrides(path.as_ref(), executor)?; + Ok(Self { overrides }) + } + + /// Gets an override by it's runtime spec version. + /// + /// Returns `None` if an override for a spec version does not exist. + pub fn get<'a, 'b: 'a>( + &'b self, + spec: &u32, + pages: Option, + spec_name: &str, + ) -> Option<(RuntimeCode<'a>, RuntimeVersion)> { + self.overrides.get(spec).and_then(|w| { + if spec_name == &*w.version.spec_name { + Some((w.runtime_code(pages), w.version.clone())) + } else { + let mut last_warn = w.last_warn.lock(); + let now = Instant::now(); + + if last_warn.is_none_or(|l| l + WARN_INTERVAL <= now) { + *last_warn = Some(now); + + tracing::warn!( + target = "wasm_overrides", + on_chain_spec_name = %spec_name, + override_spec_name = %w.version, + spec_version = %spec, + wasm_file = %w.path.display(), + "On chain and override `spec_name` do not match! Ignoring override.", + ); + } + + None + } + }) + } + + /// Scrapes a folder for WASM runtimes. + /// Returns a hashmap of the runtime version and wasm runtime code. + fn scrape_overrides(dir: &Path, executor: &E) -> Result> + where + E: RuntimeVersionOf, + { + let handle_err = |e: std::io::Error| -> sp_blockchain::Error { + WasmOverrideError::Io(dir.to_owned(), e).into() + }; + + if !dir.is_dir() { + return Err(WasmOverrideError::NotADirectory(dir.to_owned()).into()); + } + + let mut overrides = HashMap::new(); + let mut duplicates = Vec::new(); + for entry in fs::read_dir(dir).map_err(handle_err)? { + let entry = entry.map_err(handle_err)?; + let path = entry.path(); + if let Some("wasm") = path.extension().and_then(|e| e.to_str()) { + let code = fs::read(&path).map_err(handle_err)?; + let code_hash = make_hash(&code); + let version = Self::runtime_version(executor, &code, &code_hash, Some(128))?; + tracing::info!( + target: "wasm_overrides", + version = %version, + file = %path.display(), + "Found wasm override.", + ); + + let wasm = WasmBlob::new(code, code_hash, path.clone(), version.clone()); + + if let Some(other) = overrides.insert(version.spec_version, wasm) { + tracing::info!( + target: "wasm_overrides", + first = %other.path.display(), + second = %path.display(), + %version, + "Found duplicate spec version for runtime.", + ); + duplicates.push(path.display().to_string()); + } + } + } + + if !duplicates.is_empty() { + return Err(WasmOverrideError::DuplicateRuntime(duplicates).into()); + } + + Ok(overrides) + } + + fn runtime_version( + executor: &E, + code: &[u8], + code_hash: &[u8], + heap_pages: Option, + ) -> Result + where + E: RuntimeVersionOf, + { + let mut ext = BasicExternalities::default(); + executor + .runtime_version( + &mut ext, + &RuntimeCode { + code_fetcher: &WrappedRuntimeCode(code.into()), + heap_pages, + hash: code_hash.into(), + }, + ) + .map_err(|e| WasmOverrideError::VersionInvalid(e.to_string()).into()) + } +} + +/// Returns a WasmOverride struct filled with dummy data for testing. +#[cfg(test)] +pub fn dummy_overrides() -> WasmOverride { + let version = RuntimeVersion { + spec_name: "test".into(), + ..Default::default() + }; + let mut overrides = HashMap::new(); + overrides.insert( + 0, + WasmBlob::new( + vec![0, 0, 0, 0, 0, 0, 0, 0], + vec![0], + PathBuf::new(), + version.clone(), + ), + ); + overrides.insert( + 1, + WasmBlob::new( + vec![1, 1, 1, 1, 1, 1, 1, 1], + vec![1], + PathBuf::new(), + version.clone(), + ), + ); + overrides.insert( + 2, + WasmBlob::new( + vec![2, 2, 2, 2, 2, 2, 2, 2], + vec![2], + PathBuf::new(), + version, + ), + ); + + WasmOverride { overrides } +} + +#[cfg(test)] +mod tests { + use super::*; + use sc_executor::{HeapAllocStrategy, WasmExecutor}; + use std::fs::{self, File}; + + fn executor() -> WasmExecutor { + WasmExecutor::builder() + .with_onchain_heap_alloc_strategy(HeapAllocStrategy::Static { extra_pages: 128 }) + .with_offchain_heap_alloc_strategy(HeapAllocStrategy::Static { extra_pages: 128 }) + .with_max_runtime_instances(1) + .with_runtime_cache_size(2) + .build() + } + + fn wasm_test(fun: F) + where + F: Fn(&Path, &[u8], &WasmExecutor), + { + let exec = executor(); + let bytes = substrate_test_runtime::wasm_binary_unwrap(); + let dir = tempfile::tempdir().expect("Create a temporary directory"); + fun(dir.path(), bytes, &exec); + dir.close().expect("Temporary Directory should close"); + } + + #[test] + fn should_get_runtime_version() { + let executor = executor(); + + let version = WasmOverride::runtime_version( + &executor, + substrate_test_runtime::wasm_binary_unwrap(), + &[1], + Some(128), + ) + .expect("should get the `RuntimeVersion` of the test-runtime wasm blob"); + assert_eq!(version.spec_version, 2); + } + + #[test] + fn should_scrape_wasm() { + wasm_test(|dir, wasm_bytes, exec| { + fs::write(dir.join("test.wasm"), wasm_bytes).expect("Create test file"); + let overrides = + WasmOverride::scrape_overrides(dir, exec).expect("HashMap of u32 and WasmBlob"); + let wasm = overrides.get(&2).expect("WASM binary"); + assert_eq!( + wasm.code, + substrate_test_runtime::wasm_binary_unwrap().to_vec() + ) + }); + } + + #[test] + fn should_check_for_duplicates() { + wasm_test(|dir, wasm_bytes, exec| { + fs::write(dir.join("test0.wasm"), wasm_bytes).expect("Create test file"); + fs::write(dir.join("test1.wasm"), wasm_bytes).expect("Create test file"); + let scraped = WasmOverride::scrape_overrides(dir, exec); + + match scraped { + Err(sp_blockchain::Error::Application(e)) => { + match e.downcast_ref::() { + Some(WasmOverrideError::DuplicateRuntime(duplicates)) => { + assert_eq!(duplicates.len(), 1); + } + _ => panic!("Test should end with Msg Error Variant"), + } + } + _ => panic!("Test should end in error"), + } + }); + } + + #[test] + fn should_ignore_non_wasm() { + wasm_test(|dir, wasm_bytes, exec| { + File::create(dir.join("README.md")).expect("Create test file"); + File::create(dir.join("LICENSE")).expect("Create a test file"); + fs::write(dir.join("test0.wasm"), wasm_bytes).expect("Create test file"); + let scraped = + WasmOverride::scrape_overrides(dir, exec).expect("HashMap of u32 and WasmBlob"); + assert_eq!(scraped.len(), 1); + }); + } +} diff --git a/substrate/service/src/client/wasm_substitutes.rs b/substrate/service/src/client/wasm_substitutes.rs new file mode 100644 index 00000000000..c851050d572 --- /dev/null +++ b/substrate/service/src/client/wasm_substitutes.rs @@ -0,0 +1,161 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! # WASM substitutes + +use sc_client_api::backend; +use sc_executor::RuntimeVersionOf; +use sp_blockchain::{HeaderBackend, Result}; +use sp_core::traits::{FetchRuntimeCode, RuntimeCode, WrappedRuntimeCode}; +use sp_runtime::traits::{Block as BlockT, NumberFor}; +use sp_state_machine::BasicExternalities; +use sp_version::RuntimeVersion; +use std::{ + collections::{hash_map::DefaultHasher, HashMap}, + hash::Hasher as _, + sync::Arc, +}; + +/// A wasm substitute for the on chain wasm. +#[derive(Debug)] +struct WasmSubstitute { + code: Vec, + hash: Vec, + /// The block number on which we should start using the substitute. + block_number: NumberFor, + version: RuntimeVersion, +} + +impl WasmSubstitute { + fn new(code: Vec, block_number: NumberFor, version: RuntimeVersion) -> Self { + let hash = make_hash(&code); + Self { + code, + hash, + block_number, + version, + } + } + + fn runtime_code(&self, heap_pages: Option) -> RuntimeCode<'_> { + RuntimeCode { + code_fetcher: self, + hash: self.hash.clone(), + heap_pages, + } + } + + /// Returns `true` when the substitute matches for the given `hash`. + fn matches( + &self, + hash: ::Hash, + backend: &impl backend::Backend, + ) -> bool { + let requested_block_number = backend.blockchain().number(hash).ok().flatten(); + + Some(self.block_number) <= requested_block_number + } +} + +/// Make a hash out of a byte string using the default rust hasher +fn make_hash(val: &K) -> Vec { + let mut state = DefaultHasher::new(); + val.hash(&mut state); + state.finish().to_le_bytes().to_vec() +} + +impl FetchRuntimeCode for WasmSubstitute { + fn fetch_runtime_code(&self) -> Option> { + Some(self.code.as_slice().into()) + } +} + +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum WasmSubstituteError { + #[error("Failed to get runtime version: {0}")] + VersionInvalid(String), +} + +impl From for sp_blockchain::Error { + fn from(err: WasmSubstituteError) -> Self { + Self::Application(Box::new(err)) + } +} + +/// Substitutes the on-chain wasm with some hard coded blobs. +#[derive(Debug)] +pub struct WasmSubstitutes { + /// spec_version -> WasmSubstitute + substitutes: Arc>>, + executor: Arc, + backend: Arc, +} + +impl Clone for WasmSubstitutes { + fn clone(&self) -> Self { + Self { + substitutes: self.substitutes.clone(), + executor: self.executor.clone(), + backend: self.backend.clone(), + } + } +} + +impl WasmSubstitutes +where + Executor: RuntimeVersionOf, + Backend: backend::Backend, + Block: BlockT, +{ + /// Create a new instance. + pub fn new( + substitutes: HashMap, Vec>, + executor: Arc, + backend: Arc, + ) -> Result { + let substitutes = substitutes + .into_iter() + .map(|(block_number, code)| { + let runtime_code = RuntimeCode { + code_fetcher: &WrappedRuntimeCode((&code).into()), + heap_pages: None, + hash: make_hash(&code), + }; + let version = Self::runtime_version(&executor, &runtime_code)?; + let spec_version = version.spec_version; + + let substitute = WasmSubstitute::new(code, block_number, version); + + Ok((spec_version, substitute)) + }) + .collect::>>()?; + + Ok(Self { + executor, + substitutes: Arc::new(substitutes), + backend, + }) + } + + /// Get a substitute. + /// + /// Returns `None` if there isn't any substitute required. + pub fn get( + &self, + spec: u32, + pages: Option, + hash: Block::Hash, + ) -> Option<(RuntimeCode<'_>, RuntimeVersion)> { + let s = self.substitutes.get(&spec)?; + s.matches(hash, &*self.backend) + .then(|| (s.runtime_code(pages), s.version.clone())) + } + + fn runtime_version(executor: &Executor, code: &RuntimeCode) -> Result { + let mut ext = BasicExternalities::default(); + executor + .runtime_version(&mut ext, code) + .map_err(|e| WasmSubstituteError::VersionInvalid(e.to_string()).into()) + } +} diff --git a/substrate/service/src/config.rs b/substrate/service/src/config.rs new file mode 100644 index 00000000000..8727531ef53 --- /dev/null +++ b/substrate/service/src/config.rs @@ -0,0 +1,347 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Service configuration. + +pub use jsonrpsee::server::BatchRequestConfig as RpcBatchRequestConfig; +use prometheus_endpoint::Registry; +use sc_chain_spec::ChainSpec; +pub use sc_client_db::{BlocksPruning, Database, DatabaseSource, PruningMode}; +pub use sc_executor::{WasmExecutionMethod, WasmtimeInstantiationStrategy}; +pub use sc_network::{ + config::{ + MultiaddrWithPeerId, NetworkConfiguration, NodeKeyConfig, NonDefaultSetConfig, ProtocolId, + Role, SetConfig, SyncMode, TransportConfig, + }, + request_responses::{ + IncomingRequest, OutgoingResponse, ProtocolConfig as RequestResponseConfig, + }, + Multiaddr, +}; +pub use sc_rpc_server::{ + IpNetwork, RpcEndpoint, RpcMethodLimit, RpcMethods, + SubscriptionIdProvider as RpcSubscriptionIdProvider, +}; +pub use sc_telemetry::TelemetryEndpoints; +pub use sc_transaction_pool::Options as TransactionPoolOptions; +use sp_core::crypto::SecretString; +use std::{ + io, iter, + net::SocketAddr, + num::NonZeroU32, + path::{Path, PathBuf}, +}; +use tempfile::TempDir; + +/// Service configuration. +#[derive(Debug)] +pub struct Configuration { + /// Implementation name + pub impl_name: String, + /// Implementation version (see sc-cli to see an example of format) + pub impl_version: String, + /// Node role. + pub role: Role, + /// Handle to the tokio runtime. Will be used to spawn futures by the task manager. + pub tokio_handle: tokio::runtime::Handle, + /// Extrinsic pool configuration. + pub transaction_pool: TransactionPoolOptions, + /// Network configuration. + pub network: NetworkConfiguration, + /// Configuration for the keystore. + pub keystore: KeystoreConfig, + /// Configuration for the database. + pub database: DatabaseSource, + /// Maximum size of internal trie cache in bytes. + /// + /// If `None` is given the cache is disabled. + pub trie_cache_maximum_size: Option, + /// State pruning settings. + pub state_pruning: Option, + /// Number of blocks to keep in the db. + /// + /// NOTE: only finalized blocks are subject for removal! + pub blocks_pruning: BlocksPruning, + /// Chain configuration. + pub chain_spec: Box, + /// Runtime executor configuration. + pub executor: ExecutorConfiguration, + /// Directory where local WASM runtimes live. These runtimes take precedence + /// over on-chain runtimes when the spec version matches. Set to `None` to + /// disable overrides (default). + pub wasm_runtime_overrides: Option, + /// RPC configuration. + pub rpc: RpcConfiguration, + /// Prometheus endpoint configuration. `None` if disabled. + pub prometheus_config: Option, + /// Telemetry service URL. `None` if disabled. + pub telemetry_endpoints: Option, + /// Should offchain workers be executed. + pub offchain_worker: OffchainWorkerConfig, + /// Enable authoring even when offline. + pub force_authoring: bool, + /// Disable GRANDPA when running in validator mode + pub disable_grandpa: bool, + /// Development key seed. + /// + /// When running in development mode, the seed will be used to generate authority keys by the + /// keystore. + /// + /// Should only be set when `node` is running development mode. + pub dev_key_seed: Option, + /// Tracing targets + pub tracing_targets: Option, + /// Tracing receiver + pub tracing_receiver: sc_tracing::TracingReceiver, + /// Announce block automatically after they have been imported + pub announce_block: bool, + /// Data path root for the configured chain. + pub data_path: PathBuf, + /// Base path of the configuration. This is shared between chains. + pub base_path: BasePath, +} + +/// Type for tasks spawned by the executor. +#[derive(PartialEq)] +pub enum TaskType { + /// Regular non-blocking futures. Polling the task is expected to be a lightweight operation. + Async, + /// The task might perform a lot of expensive CPU operations and/or call `thread::sleep`. + Blocking, +} + +/// Configuration of the client keystore. +#[derive(Debug, Clone)] +pub enum KeystoreConfig { + /// Keystore at a path on-disk. Recommended for native nodes. + Path { + /// The path of the keystore. + path: PathBuf, + /// Node keystore's password. + password: Option, + }, + /// In-memory keystore. Recommended for in-browser nodes. + InMemory, +} + +impl KeystoreConfig { + /// Returns the path for the keystore. + pub fn path(&self) -> Option<&Path> { + match self { + Self::Path { path, .. } => Some(path), + Self::InMemory => None, + } + } +} +/// Configuration of the database of the client. +#[derive(Debug, Clone, Default)] +pub struct OffchainWorkerConfig { + /// If this is allowed. + pub enabled: bool, + /// allow writes from the runtime to the offchain worker database. + pub indexing_enabled: bool, +} + +/// Configuration of the Prometheus endpoint. +#[derive(Debug, Clone)] +pub struct PrometheusConfig { + /// Port to use. + pub port: SocketAddr, + /// A metrics registry to use. Useful for setting the metric prefix. + pub registry: Registry, +} + +impl PrometheusConfig { + /// Create a new config using the default registry. + pub fn new_with_default_registry(port: SocketAddr, chain_id: String) -> Self { + let param = iter::once((String::from("chain"), chain_id)).collect(); + Self { + port, + registry: Registry::new_custom(None, Some(param)) + .expect("this can only fail if the prefix is empty"), + } + } +} + +impl Configuration { + /// Returns a string displaying the node role. + pub fn display_role(&self) -> String { + self.role.to_string() + } + + /// Returns the prometheus metrics registry, if available. + pub fn prometheus_registry(&self) -> Option<&Registry> { + self.prometheus_config + .as_ref() + .map(|config| &config.registry) + } + + /// Returns the network protocol id from the chain spec, or the default. + pub fn protocol_id(&self) -> ProtocolId { + let protocol_id_full = match self.chain_spec.protocol_id() { + Some(pid) => pid, + None => { + log::warn!( + "Using default protocol ID {:?} because none is configured in the \ + chain specs", + crate::DEFAULT_PROTOCOL_ID + ); + crate::DEFAULT_PROTOCOL_ID + } + }; + ProtocolId::from(protocol_id_full) + } + + /// Returns true if the genesis state writing will be skipped while initializing the genesis + /// block. + pub fn no_genesis(&self) -> bool { + matches!( + self.network.sync_mode, + SyncMode::LightState { .. } | SyncMode::Warp + ) + } + + /// Returns the database config for creating the backend. + pub fn db_config(&self) -> sc_client_db::DatabaseSettings { + sc_client_db::DatabaseSettings { + trie_cache_maximum_size: self.trie_cache_maximum_size, + state_pruning: self.state_pruning.clone(), + source: self.database.clone(), + blocks_pruning: self.blocks_pruning, + } + } +} + +#[static_init::dynamic(drop, lazy)] +static mut BASE_PATH_TEMP: Option = None; + +/// The base path that is used for everything that needs to be written on disk to run a node. +#[derive(Clone, Debug)] +pub struct BasePath { + path: PathBuf, +} + +impl BasePath { + /// Create a `BasePath` instance using a temporary directory prefixed with "substrate" and use + /// it as base path. + /// + /// Note: The temporary directory will be created automatically and deleted when the program + /// exits. Every call to this function will return the same path for the lifetime of the + /// program. + pub fn new_temp_dir() -> io::Result { + let mut temp = BASE_PATH_TEMP.write(); + + match &*temp { + Some(p) => Ok(Self::new(p.path())), + None => { + let temp_dir = tempfile::Builder::new().prefix("substrate").tempdir()?; + let path = PathBuf::from(temp_dir.path()); + + *temp = Some(temp_dir); + Ok(Self::new(path)) + } + } + } + + /// Create a `BasePath` instance based on an existing path on disk. + /// + /// Note: this function will not ensure that the directory exist nor create the directory. It + /// will also not delete the directory when the instance is dropped. + pub fn new>(path: P) -> BasePath { + Self { path: path.into() } + } + + /// Create a base path from values describing the project. + pub fn from_project(qualifier: &str, organization: &str, application: &str) -> BasePath { + BasePath::new( + directories::ProjectDirs::from(qualifier, organization, application) + .expect("app directories exist on all supported platforms; qed") + .data_local_dir(), + ) + } + + /// Retrieve the base path. + pub fn path(&self) -> &Path { + &self.path + } + + /// Returns the configuration directory inside this base path. + /// + /// The path looks like `$base_path/chains/$chain_id` + pub fn config_dir(&self, chain_id: &str) -> PathBuf { + self.path().join("chains").join(chain_id) + } +} + +impl From for BasePath { + fn from(path: PathBuf) -> Self { + BasePath::new(path) + } +} + +/// RPC configuration. +#[derive(Debug)] +pub struct RpcConfiguration { + /// JSON-RPC server endpoints. + pub addr: Option>, + /// Maximum number of connections for JSON-RPC server. + pub max_connections: u32, + /// CORS settings for HTTP & WS servers. `None` if all origins are allowed. + pub cors: Option>, + /// RPC methods to expose (by default only a safe subset or all of them). + pub methods: RpcMethods, + /// Maximum payload of a rpc request + pub max_request_size: u32, + /// Maximum payload of a rpc response. + pub max_response_size: u32, + /// Custom JSON-RPC subscription ID provider. + /// + /// Default: [`crate::RandomStringSubscriptionId`]. + pub id_provider: Option>, + /// Maximum allowed subscriptions per rpc connection + pub max_subs_per_conn: u32, + /// JSON-RPC server default port. + pub port: u16, + /// The number of messages the JSON-RPC server is allowed to keep in memory. + pub message_buffer_capacity: u32, + /// JSON-RPC server batch config. + pub batch_config: RpcBatchRequestConfig, + /// Node-wide per-method rate and concurrency budgets. + /// + /// Each entry configures one or more literal RPC method names in the form + /// `METHOD[,ALIAS...]=CALLS_PER_MINUTE,MAX_IN_FLIGHT`. Aliases share the + /// same budget. An empty vector leaves all methods unlimited. + pub method_limits: Vec, + /// RPC rate limit per minute. + pub rate_limit: Option, + /// RPC rate limit whitelisted ip addresses. + pub rate_limit_whitelisted_ips: Vec, + /// RPC rate limit trust proxy headers. + pub rate_limit_trust_proxy_headers: bool, +} + +/// Runtime executor configuration. +#[derive(Debug, Clone)] +pub struct ExecutorConfiguration { + /// Wasm execution method. + pub wasm_method: WasmExecutionMethod, + /// The size of the instances cache. + /// + /// The default value is 8. + pub max_runtime_instances: usize, + /// The default number of 64KB pages to allocate for Wasm execution + pub default_heap_pages: Option, + /// Maximum number of different runtime versions that can be cached. + pub runtime_cache_size: u8, +} + +impl Default for ExecutorConfiguration { + fn default() -> Self { + Self { + wasm_method: WasmExecutionMethod::default(), + max_runtime_instances: 8, + default_heap_pages: None, + runtime_cache_size: 2, + } + } +} diff --git a/substrate/service/src/error.rs b/substrate/service/src/error.rs new file mode 100644 index 00000000000..58a6a1e5625 --- /dev/null +++ b/substrate/service/src/error.rs @@ -0,0 +1,62 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Errors that can occur during the service operation. + +use sc_keystore; +use sp_blockchain; +use sp_consensus; + +/// Service Result typedef. +pub type Result = std::result::Result; + +/// Service errors. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +#[non_exhaustive] +pub enum Error { + #[error(transparent)] + Client(#[from] sp_blockchain::Error), + + #[error(transparent)] + Io(#[from] std::io::Error), + + #[error(transparent)] + Consensus(#[from] sp_consensus::Error), + + #[error(transparent)] + Network(#[from] sc_network::error::Error), + + #[error(transparent)] + Keystore(#[from] sc_keystore::Error), + + #[error(transparent)] + Telemetry(#[from] sc_telemetry::Error), + + #[error("Best chain selection strategy (SelectChain) is not provided.")] + SelectChainRequired, + + #[error("Tasks executor hasn't been provided.")] + TaskExecutorRequired, + + #[error("Prometheus metrics error: {0}")] + Prometheus(#[from] prometheus_endpoint::PrometheusError), + + #[error("Application: {0}")] + Application(#[from] Box), + + #[error("Other: {0}")] + Other(String), +} + +impl<'a> From<&'a str> for Error { + fn from(s: &'a str) -> Self { + Error::Other(s.into()) + } +} + +impl From for Error { + fn from(s: String) -> Self { + Error::Other(s) + } +} diff --git a/substrate/service/src/lib.rs b/substrate/service/src/lib.rs new file mode 100644 index 00000000000..2ad45e42a63 --- /dev/null +++ b/substrate/service/src/lib.rs @@ -0,0 +1,634 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Substrate service. Starts a thread that spins up the network, client, and extrinsic pool. +//! Manages communication between them. + +#![warn(missing_docs)] +#![recursion_limit = "1024"] +// These structural lints are required by the pinned Polkadot SDK API; changing the signatures would break API compatibility. +#![allow( + clippy::result_large_err, + clippy::too_many_arguments, + clippy::type_complexity, + clippy::module_inception, + clippy::extra_unused_type_parameters +)] + +pub mod chain_ops; +pub mod config; +pub mod error; + +mod builder; +#[cfg(feature = "test-helpers")] +pub mod client; +#[cfg(not(feature = "test-helpers"))] +mod client; +mod metrics; +mod task_manager; + +use std::{ + collections::HashMap, + net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, +}; + +use codec::{Decode, Encode}; +use futures::{pin_mut, FutureExt, StreamExt}; +use jsonrpsee::RpcModule; +use log::{debug, error, warn}; +use sc_client_api::{blockchain::HeaderBackend, BlockBackend, BlockchainEvents, ProofProvider}; +use sc_network::{ + config::MultiaddrWithPeerId, service::traits::NetworkService, NetworkBackend, NetworkBlock, + NetworkPeers, NetworkStateInfo, +}; +use sc_network_sync::SyncingService; +use sc_network_types::PeerId; +use sc_utils::mpsc::TracingUnboundedReceiver; +use sp_blockchain::HeaderMetadata; +use sp_consensus::SyncOracle; +use sp_runtime::traits::{Block as BlockT, Header as HeaderT}; + +pub use self::{ + builder::{ + build_network, gen_rpc_module, init_telemetry, new_client, new_db_backend, new_full_client, + new_full_parts, new_full_parts_record_import, new_full_parts_with_genesis_builder, + new_wasm_executor, propagate_transaction_notifications, spawn_tasks, BuildNetworkParams, + KeystoreContainer, NetworkStarter, SpawnTasksParams, TFullBackend, TFullCallExecutor, + TFullClient, + }, + client::{ClientConfig, LocalCallExecutor}, + error::Error, + metrics::MetricsService, +}; +#[allow(deprecated)] +pub use builder::new_native_or_wasm_executor; + +pub use sc_chain_spec::{ + construct_genesis_block, resolve_state_version_from_wasm, BuildGenesisBlock, + GenesisBlockBuilder, +}; + +pub use config::{ + BasePath, BlocksPruning, Configuration, DatabaseSource, PruningMode, Role, RpcMethods, TaskType, +}; +pub use sc_chain_spec::{ + ChainSpec, ChainType, Extension as ChainSpecExtension, GenericChainSpec, NoExtension, + Properties, +}; + +use crate::config::RpcConfiguration; +use prometheus_endpoint::Registry; +pub use sc_consensus::ImportQueue; +pub use sc_executor::NativeExecutionDispatch; +pub use sc_network_sync::WarpSyncConfig; +#[doc(hidden)] +pub use sc_network_transactions::config::{TransactionImport, TransactionImportFuture}; +pub use sc_rpc::{RandomIntegerSubscriptionId, RandomStringSubscriptionId}; +pub use sc_tracing::TracingReceiver; +pub use sc_transaction_pool::Options as TransactionPoolOptions; +pub use sc_transaction_pool_api::{error::IntoPoolError, InPoolTransaction, TransactionPool}; +#[doc(hidden)] +pub use std::{ops::Deref, result::Result, sync::Arc}; +pub use task_manager::{SpawnTaskHandle, Task, TaskManager, TaskRegistry, DEFAULT_GROUP_NAME}; +use tokio::runtime::Handle; + +const DEFAULT_PROTOCOL_ID: &str = "sup"; + +/// RPC handlers that can perform RPC queries. +#[derive(Clone)] +pub struct RpcHandlers(Arc>); + +impl RpcHandlers { + /// Create PRC handlers instance. + pub fn new(inner: Arc>) -> Self { + Self(inner) + } + + /// Starts an RPC query. + /// + /// The query is passed as a string and must be valid JSON-RPC request object. + /// + /// Returns a response and a stream if the call successful, fails if the + /// query could not be decoded as a JSON-RPC request object. + /// + /// If the request subscribes you to events, the `stream` can be used to + /// retrieve the events. + pub async fn rpc_query( + &self, + json_query: &str, + ) -> Result<(String, tokio::sync::mpsc::Receiver), serde_json::Error> { + // Because `tokio::sync::mpsc::channel` is used under the hood + // it will panic if it's set to usize::MAX. + // + // This limit is used to prevent panics and is large enough. + const TOKIO_MPSC_MAX_SIZE: usize = tokio::sync::Semaphore::MAX_PERMITS; + + self.0 + .raw_json_request(json_query, TOKIO_MPSC_MAX_SIZE) + .await + } + + /// Provides access to the underlying `RpcModule` + pub fn handle(&self) -> Arc> { + self.0.clone() + } +} + +/// An incomplete set of chain components, but enough to run the chain ops subcommands. +pub struct PartialComponents { + /// A shared client instance. + pub client: Arc, + /// A shared backend instance. + pub backend: Arc, + /// The chain task manager. + pub task_manager: TaskManager, + /// A keystore container instance. + pub keystore_container: KeystoreContainer, + /// A chain selection algorithm instance. + pub select_chain: SelectChain, + /// An import queue. + pub import_queue: ImportQueue, + /// A shared transaction pool. + pub transaction_pool: Arc, + /// Everything else that needs to be passed into the main build function. + pub other: Other, +} + +/// Builds a future that continuously polls the network. +async fn build_network_future< + B: BlockT, + C: BlockchainEvents + + HeaderBackend + + BlockBackend + + HeaderMetadata + + ProofProvider + + Send + + Sync + + 'static, + H: sc_network_common::ExHashT, + N: NetworkBackend::Hash>, +>( + network: N, + client: Arc, + sync_service: Arc>, + announce_imported_blocks: bool, +) { + let mut imported_blocks_stream = client.import_notification_stream().fuse(); + + // Stream of finalized blocks reported by the client. + let mut finality_notification_stream = client.finality_notification_stream().fuse(); + + let network_run = network.run().fuse(); + pin_mut!(network_run); + + loop { + futures::select! { + // List of blocks that the client has imported. + notification = imported_blocks_stream.next() => { + let notification = match notification { + Some(n) => n, + // If this stream is shut down, that means the client has shut down, and the + // most appropriate thing to do for the network future is to shut down too. + None => { + debug!("Block import stream has terminated, shutting down the network future."); + return + }, + }; + + if announce_imported_blocks { + sync_service.announce_block(notification.hash, None); + } + + if notification.is_new_best { + sync_service.new_best_block_imported( + notification.hash, + *notification.header.number(), + ); + } + } + + // List of blocks that the client has finalized. + notification = finality_notification_stream.select_next_some() => { + sync_service.on_block_finalized(notification.hash, notification.header); + } + + // Drive the network. Shut down the network future if `NetworkWorker` has terminated. + _ = network_run => { + debug!("`NetworkWorker` has terminated, shutting down the network future."); + return + } + } + } +} + +/// Builds a future that processes system RPC requests. +pub async fn build_system_rpc_future< + B: BlockT, + C: BlockchainEvents + + HeaderBackend + + BlockBackend + + HeaderMetadata + + ProofProvider + + Send + + Sync + + 'static, + H: sc_network_common::ExHashT, +>( + role: Role, + network_service: Arc, + sync_service: Arc>, + client: Arc, + mut rpc_rx: TracingUnboundedReceiver>, + should_have_peers: bool, +) { + // Current best block at initialization, to report to the RPC layer. + let starting_block = client.info().best_number; + + loop { + // Answer incoming RPC requests. + let Some(req) = rpc_rx.next().await else { + debug!("RPC requests stream has terminated, shutting down the system RPC future."); + return; + }; + + match req { + sc_rpc::system::Request::Health(sender) => match sync_service.peers_info().await { + Ok(info) => { + let _ = sender.send(sc_rpc::system::Health { + peers: info.len(), + is_syncing: sync_service.is_major_syncing(), + should_have_peers, + }); + } + Err(_) => log::error!("`SyncingEngine` shut down"), + }, + sc_rpc::system::Request::LocalPeerId(sender) => { + let _ = sender.send(network_service.local_peer_id().to_base58()); + } + sc_rpc::system::Request::LocalListenAddresses(sender) => { + let peer_id = (network_service.local_peer_id()).into(); + let p2p_proto_suffix = sc_network::multiaddr::Protocol::P2p(peer_id); + let addresses = network_service + .listen_addresses() + .iter() + .map(|addr| addr.clone().with(p2p_proto_suffix.clone()).to_string()) + .collect(); + let _ = sender.send(addresses); + } + sc_rpc::system::Request::Peers(sender) => match sync_service.peers_info().await { + Ok(info) => { + let _ = sender.send( + info.into_iter() + .map(|(peer_id, p)| sc_rpc::system::PeerInfo { + peer_id: peer_id.to_base58(), + roles: format!("{:?}", p.roles), + best_hash: p.best_hash, + best_number: p.best_number, + }) + .collect(), + ); + } + Err(_) => log::error!("`SyncingEngine` shut down"), + }, + sc_rpc::system::Request::NetworkState(sender) => { + let network_state = network_service.network_state().await; + if let Ok(network_state) = network_state { + if let Ok(network_state) = serde_json::to_value(network_state) { + let _ = sender.send(network_state); + } + } else { + break; + } + } + sc_rpc::system::Request::NetworkAddReservedPeer(peer_addr, sender) => { + let result = match MultiaddrWithPeerId::try_from(peer_addr) { + Ok(peer) => network_service.add_reserved_peer(peer), + Err(err) => Err(err.to_string()), + }; + let x = result.map_err(sc_rpc::system::error::Error::MalformattedPeerArg); + let _ = sender.send(x); + } + sc_rpc::system::Request::NetworkRemoveReservedPeer(peer_id, sender) => { + let _ = match peer_id.parse::() { + Ok(peer_id) => { + network_service.remove_reserved_peer(peer_id); + sender.send(Ok(())) + } + Err(e) => sender.send(Err(sc_rpc::system::error::Error::MalformattedPeerArg( + e.to_string(), + ))), + }; + } + sc_rpc::system::Request::NetworkReservedPeers(sender) => { + let Ok(reserved_peers) = network_service.reserved_peers().await else { + break; + }; + + let _ = sender.send( + reserved_peers + .iter() + .map(|peer_id| peer_id.to_base58()) + .collect(), + ); + } + sc_rpc::system::Request::NodeRoles(sender) => { + use sc_rpc::system::NodeRole; + + let node_role = match role { + Role::Authority => NodeRole::Authority, + Role::Full => NodeRole::Full, + }; + + let _ = sender.send(vec![node_role]); + } + sc_rpc::system::Request::SyncState(sender) => { + use sc_rpc::system::SyncState; + + match sync_service + .status() + .await + .map(|status| status.best_seen_block) + { + Ok(best_seen_block) => { + let best_number = client.info().best_number; + let _ = sender.send(SyncState { + starting_block, + current_block: best_number, + highest_block: best_seen_block.unwrap_or(best_number), + }); + } + Err(_) => log::error!("`SyncingEngine` shut down"), + } + } + } + } + + debug!("`NetworkWorker` has terminated, shutting down the system RPC future."); +} + +// Wrapper for HTTP and WS servers that makes sure they are properly shut down. +mod waiting { + pub struct Server(pub Option); + + impl Drop for Server { + fn drop(&mut self) { + if let Some(server) = self.0.take() { + // This doesn't not wait for the server to be stopped but fires the signal. + let _ = server.stop(); + } + } + } +} + +/// Starts RPC servers. +pub fn start_rpc_servers( + rpc_configuration: &RpcConfiguration, + registry: Option<&Registry>, + tokio_handle: &Handle, + gen_rpc_module: R, + rpc_id_provider: Option>, +) -> Result, error::Error> +where + R: Fn() -> Result, Error>, +{ + let endpoints: Vec = if let Some(endpoints) = + rpc_configuration.addr.as_ref() + { + endpoints.clone() + } else { + let ipv6 = SocketAddr::V6(SocketAddrV6::new( + Ipv6Addr::LOCALHOST, + rpc_configuration.port, + 0, + 0, + )); + let ipv4 = SocketAddr::V4(SocketAddrV4::new( + Ipv4Addr::LOCALHOST, + rpc_configuration.port, + )); + + vec![ + sc_rpc_server::RpcEndpoint { + batch_config: rpc_configuration.batch_config, + cors: rpc_configuration.cors.clone(), + listen_addr: ipv4, + max_buffer_capacity_per_connection: rpc_configuration.message_buffer_capacity, + max_connections: rpc_configuration.max_connections, + max_payload_in_mb: rpc_configuration.max_request_size, + max_payload_out_mb: rpc_configuration.max_response_size, + max_subscriptions_per_connection: rpc_configuration.max_subs_per_conn, + rpc_methods: rpc_configuration.methods, + rate_limit: rpc_configuration.rate_limit, + rate_limit_trust_proxy_headers: rpc_configuration.rate_limit_trust_proxy_headers, + rate_limit_whitelisted_ips: rpc_configuration.rate_limit_whitelisted_ips.clone(), + retry_random_port: true, + is_optional: false, + }, + sc_rpc_server::RpcEndpoint { + batch_config: rpc_configuration.batch_config, + cors: rpc_configuration.cors.clone(), + listen_addr: ipv6, + max_buffer_capacity_per_connection: rpc_configuration.message_buffer_capacity, + max_connections: rpc_configuration.max_connections, + max_payload_in_mb: rpc_configuration.max_request_size, + max_payload_out_mb: rpc_configuration.max_response_size, + max_subscriptions_per_connection: rpc_configuration.max_subs_per_conn, + rpc_methods: rpc_configuration.methods, + rate_limit: rpc_configuration.rate_limit, + rate_limit_trust_proxy_headers: rpc_configuration.rate_limit_trust_proxy_headers, + rate_limit_whitelisted_ips: rpc_configuration.rate_limit_whitelisted_ips.clone(), + retry_random_port: true, + is_optional: true, + }, + ] + }; + + let metrics = sc_rpc_server::RpcMetrics::new(registry)?; + let rpc_api = gen_rpc_module()?; + + let server_config = sc_rpc_server::Config { + endpoints, + rpc_api, + metrics, + id_provider: rpc_id_provider, + tokio_handle: tokio_handle.clone(), + method_limits: rpc_configuration.method_limits.clone(), + }; + + // TODO: https://github.com/paritytech/substrate/issues/13773 + // + // `block_in_place` is a hack to allow callers to call `block_on` prior to + // calling `start_rpc_servers`. + match tokio::task::block_in_place(|| { + tokio_handle.block_on(sc_rpc_server::start_server(server_config)) + }) { + Ok(server) => Ok(Box::new(waiting::Server(Some(server)))), + Err(e) => Err(Error::Application(e)), + } +} + +/// Transaction pool adapter. +pub struct TransactionPoolAdapter { + pool: Arc

, + client: Arc, +} + +impl TransactionPoolAdapter { + /// Constructs a new instance of [`TransactionPoolAdapter`]. + pub fn new(pool: Arc

, client: Arc) -> Self { + Self { pool, client } + } +} + +/// Get transactions for propagation. +/// +/// Function extracted to simplify the test and prevent creating `ServiceFactory`. +fn transactions_to_propagate(pool: &Pool) -> Vec<(H, B::Extrinsic)> +where + Pool: TransactionPool, + B: BlockT, + H: std::hash::Hash + Eq + sp_runtime::traits::Member + sp_runtime::traits::MaybeSerialize, + E: IntoPoolError + From, +{ + pool.ready() + .filter(|t| t.is_propagable()) + .map(|t| { + let hash = t.hash().clone(); + let ex: B::Extrinsic = t.data().clone(); + (hash, ex) + }) + .collect() +} + +impl sc_network_transactions::config::TransactionPool + for TransactionPoolAdapter +where + C: HeaderBackend + + BlockBackend + + HeaderMetadata + + ProofProvider + + Send + + Sync + + 'static, + Pool: 'static + TransactionPool, + B: BlockT, + H: std::hash::Hash + Eq + sp_runtime::traits::Member + sp_runtime::traits::MaybeSerialize, + E: 'static + IntoPoolError + From, +{ + fn transactions(&self) -> Vec<(H, B::Extrinsic)> { + transactions_to_propagate(&*self.pool) + } + + fn hash_of(&self, transaction: &B::Extrinsic) -> H { + self.pool.hash_of(transaction) + } + + fn import(&self, transaction: B::Extrinsic) -> TransactionImportFuture { + let encoded = transaction.encode(); + let uxt = match Decode::decode(&mut &encoded[..]) { + Ok(uxt) => uxt, + Err(e) => { + debug!("Transaction invalid: {:?}", e); + return Box::pin(futures::future::ready(TransactionImport::Bad)); + } + }; + + let import_future = self.pool.submit_one( + self.client.info().best_hash, + sc_transaction_pool_api::TransactionSource::External, + uxt, + ); + Box::pin(async move { + match import_future.await { + Ok(_) => TransactionImport::NewGood, + Err(e) => match e.into_pool_error() { + Ok(sc_transaction_pool_api::error::Error::AlreadyImported(_)) => { + TransactionImport::KnownGood + } + Ok(e) => { + debug!("Error adding transaction to the pool: {:?}", e); + TransactionImport::Bad + } + Err(e) => { + debug!("Error converting pool error: {}", e); + // it is not bad at least, just some internal node logic error, so peer is + // innocent. + TransactionImport::KnownGood + } + }, + } + }) + } + + fn on_broadcasted(&self, propagations: HashMap>) { + self.pool.on_broadcasted(propagations) + } + + fn transaction(&self, hash: &H) -> Option { + self.pool.ready_transaction(hash).and_then( + // Only propagable transactions should be resolved for network service. + |tx| { + if tx.is_propagable() { + Some(tx.data().clone()) + } else { + None + } + }, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use futures::executor::block_on; + use sc_transaction_pool::BasicPool; + use sp_consensus::SelectChain; + use substrate_test_runtime_client::{ + prelude::*, + runtime::{ExtrinsicBuilder, Transfer, TransferData}, + }; + + #[test] + fn should_not_propagate_transactions_that_are_marked_as_such() { + // given + let (client, longest_chain) = TestClientBuilder::new().build_with_longest_chain(); + let client = Arc::new(client); + let spawner = sp_core::testing::TaskExecutor::new(); + let pool = BasicPool::new_full( + Default::default(), + true.into(), + None, + spawner, + client.clone(), + ); + let source = sp_runtime::transaction_validity::TransactionSource::External; + let best = block_on(longest_chain.best_chain()).unwrap(); + let transaction = Transfer { + amount: 5, + nonce: 0, + from: AccountKeyring::Alice.into(), + to: AccountKeyring::Bob.into(), + } + .into_unchecked_extrinsic(); + block_on(pool.submit_one(best.hash(), source, transaction.clone())).unwrap(); + block_on( + pool.submit_one( + best.hash(), + source, + ExtrinsicBuilder::new_call_do_not_propagate() + .nonce(1) + .build(), + ), + ) + .unwrap(); + assert_eq!(pool.status().ready, 2); + + // when + let transactions = transactions_to_propagate(&*pool); + + // then + assert_eq!(transactions.len(), 1); + assert!(TransferData::try_from(&transactions[0].1).is_ok()); + } +} diff --git a/substrate/service/src/metrics.rs b/substrate/service/src/metrics.rs new file mode 100644 index 00000000000..4cda88d5ecf --- /dev/null +++ b/substrate/service/src/metrics.rs @@ -0,0 +1,309 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use futures_timer::Delay; +use prometheus_endpoint::{register, Gauge, GaugeVec, Opts, PrometheusError, Registry, U64}; +use sc_client_api::{ClientInfo, UsageProvider}; +use sc_network::{config::Role, NetworkStatus, NetworkStatusProvider}; +use sc_network_sync::{SyncStatus, SyncStatusProvider}; +use sc_telemetry::{telemetry, TelemetryHandle, SUBSTRATE_INFO}; +use sc_transaction_pool_api::{MaintainedTransactionPool, PoolStatus}; +use sc_utils::metrics::register_globals; +use sp_api::ProvideRuntimeApi; +use sp_runtime::traits::{Block, NumberFor, SaturatedConversion, UniqueSaturatedInto}; +use std::{ + sync::Arc, + time::{Duration, Instant, SystemTime}, +}; + +struct PrometheusMetrics { + // generic info + block_height: GaugeVec, + number_leaves: Gauge, + ready_transactions_number: Gauge, + + // I/O + database_cache: Gauge, + state_cache: Gauge, +} + +impl PrometheusMetrics { + fn setup( + registry: &Registry, + name: &str, + version: &str, + roles: u64, + ) -> Result { + register( + Gauge::::with_opts( + Opts::new( + "substrate_build_info", + "A metric with a constant '1' value labeled by name, version", + ) + .const_label("name", name) + .const_label("version", version), + )?, + registry, + )? + .set(1); + + register( + Gauge::::new("substrate_node_roles", "The roles the node is running as")?, + registry, + )? + .set(roles); + + register_globals(registry)?; + + let start_time_since_epoch = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_default(); + register( + Gauge::::new( + "substrate_process_start_time_seconds", + "Number of seconds between the UNIX epoch and the moment the process started", + )?, + registry, + )? + .set(start_time_since_epoch.as_secs()); + + Ok(Self { + // generic internals + block_height: register( + GaugeVec::new( + Opts::new("substrate_block_height", "Block height info of the chain"), + &["status"], + )?, + registry, + )?, + + number_leaves: register( + Gauge::new( + "substrate_number_leaves", + "Number of known chain leaves (aka forks)", + )?, + registry, + )?, + + ready_transactions_number: register( + Gauge::new( + "substrate_ready_transactions_number", + "Number of transactions in the ready queue", + )?, + registry, + )?, + + // I/ O + database_cache: register( + Gauge::new( + "substrate_database_cache_bytes", + "RocksDB cache size in bytes", + )?, + registry, + )?, + state_cache: register( + Gauge::new("substrate_state_cache_bytes", "State cache size in bytes")?, + registry, + )?, + }) + } +} + +/// A `MetricsService` periodically sends general client and +/// network state to the telemetry as well as (optionally) +/// a Prometheus endpoint. +pub struct MetricsService { + metrics: Option, + last_update: Instant, + last_total_bytes_inbound: u64, + last_total_bytes_outbound: u64, + telemetry: Option, +} + +impl MetricsService { + /// Creates a `MetricsService` that only sends information + /// to the telemetry. + pub fn new(telemetry: Option) -> Self { + MetricsService { + metrics: None, + last_total_bytes_inbound: 0, + last_total_bytes_outbound: 0, + last_update: Instant::now(), + telemetry, + } + } + + /// Creates a `MetricsService` that sends metrics + /// to prometheus alongside the telemetry. + pub fn with_prometheus( + telemetry: Option, + registry: &Registry, + role: Role, + node_name: &str, + impl_version: &str, + ) -> Result { + let role_bits = match role { + Role::Full => 1u64, + // 2u64 used to represent light client role + Role::Authority => 4u64, + }; + + PrometheusMetrics::setup(registry, node_name, impl_version, role_bits).map(|p| { + MetricsService { + metrics: Some(p), + last_total_bytes_inbound: 0, + last_total_bytes_outbound: 0, + last_update: Instant::now(), + telemetry, + } + }) + } + + /// Returns a never-ending `Future` that performs the + /// metric and telemetry updates with information from + /// the given sources. + pub async fn run( + mut self, + client: Arc, + transactions: Arc, + network: TNet, + syncing: TSync, + ) where + TBl: Block, + TCl: ProvideRuntimeApi + UsageProvider, + TExPool: MaintainedTransactionPool::Hash>, + TNet: NetworkStatusProvider, + TSync: SyncStatusProvider, + { + let mut timer = Delay::new(Duration::from_secs(0)); + let timer_interval = Duration::from_secs(5); + + loop { + // Wait for the next tick of the timer. + (&mut timer).await; + + // Try to get the latest network information. + let net_status = network.status().await.ok(); + + // Try to get the latest syncing information. + let sync_status = syncing.status().await.ok(); + + // Update / Send the metrics. + self.update( + &client.usage_info(), + &transactions.status(), + net_status, + sync_status, + ); + + // Schedule next tick. + timer.reset(timer_interval); + } + } + + fn update( + &mut self, + info: &ClientInfo, + txpool_status: &PoolStatus, + net_status: Option, + sync_status: Option>, + ) { + let now = Instant::now(); + let elapsed = (now - self.last_update).as_secs(); + self.last_update = now; + + let best_number = info.chain.best_number.saturated_into::(); + let best_hash = info.chain.best_hash; + let finalized_number: u64 = info.chain.finalized_number.saturated_into::(); + + // Update/send metrics that are always available. + telemetry!( + self.telemetry; + SUBSTRATE_INFO; + "system.interval"; + "height" => best_number, + "best" => ?best_hash, + "txcount" => txpool_status.ready, + "finalized_height" => finalized_number, + "finalized_hash" => ?info.chain.finalized_hash, + "used_state_cache_size" => info.usage.as_ref() + .map(|usage| usage.memory.state_cache.as_bytes()) + .unwrap_or(0), + ); + + if let Some(metrics) = self.metrics.as_ref() { + metrics + .block_height + .with_label_values(&["finalized"]) + .set(finalized_number); + metrics + .block_height + .with_label_values(&["best"]) + .set(best_number); + + if let Ok(leaves) = u64::try_from(info.chain.number_leaves) { + metrics.number_leaves.set(leaves); + } + + metrics + .ready_transactions_number + .set(txpool_status.ready as u64); + + if let Some(info) = info.usage.as_ref() { + metrics + .database_cache + .set(info.memory.database_cache.as_bytes() as u64); + metrics + .state_cache + .set(info.memory.state_cache.as_bytes() as u64); + } + } + + // Update/send network status information, if any. + if let Some(net_status) = net_status { + let num_peers = net_status.num_connected_peers; + let total_bytes_inbound = net_status.total_bytes_inbound; + let total_bytes_outbound = net_status.total_bytes_outbound; + + let diff_bytes_inbound = total_bytes_inbound - self.last_total_bytes_inbound; + let diff_bytes_outbound = total_bytes_outbound - self.last_total_bytes_outbound; + let (avg_bytes_per_sec_inbound, avg_bytes_per_sec_outbound) = if elapsed > 0 { + self.last_total_bytes_inbound = total_bytes_inbound; + self.last_total_bytes_outbound = total_bytes_outbound; + ( + diff_bytes_inbound + .checked_div(elapsed) + .unwrap_or(diff_bytes_inbound), + diff_bytes_outbound + .checked_div(elapsed) + .unwrap_or(diff_bytes_outbound), + ) + } else { + (diff_bytes_inbound, diff_bytes_outbound) + }; + + telemetry!( + self.telemetry; + SUBSTRATE_INFO; + "system.interval"; + "peers" => num_peers, + "bandwidth_download" => avg_bytes_per_sec_inbound, + "bandwidth_upload" => avg_bytes_per_sec_outbound, + ); + } + + if let Some(sync_status) = sync_status { + if let Some(metrics) = self.metrics.as_ref() { + let best_seen_block: Option = + sync_status.best_seen_block.map(|num: NumberFor| { + UniqueSaturatedInto::::unique_saturated_into(num) + }); + + metrics + .block_height + .with_label_values(&["sync_target"]) + .set(best_seen_block.unwrap_or(best_number)); + } + } + } +} diff --git a/substrate/service/src/task_manager/mod.rs b/substrate/service/src/task_manager/mod.rs new file mode 100644 index 00000000000..eb9aab08cfc --- /dev/null +++ b/substrate/service/src/task_manager/mod.rs @@ -0,0 +1,552 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Substrate service tasks management module. + +use crate::{config::TaskType, Error}; +use exit_future::Signal; +use futures::{ + future::{pending, select, try_join_all, BoxFuture, Either}, + Future, FutureExt, StreamExt, +}; +use parking_lot::Mutex; +use prometheus_endpoint::{ + exponential_buckets, register, CounterVec, HistogramOpts, HistogramVec, Opts, PrometheusError, + Registry, U64, +}; +use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender}; +use std::{ + collections::{hash_map::Entry, HashMap}, + panic, + pin::Pin, + result::Result, + sync::Arc, +}; +use tokio::runtime::Handle; +use tracing_futures::Instrument; + +mod prometheus_future; +#[cfg(test)] +mod tests; + +/// Default task group name. +pub const DEFAULT_GROUP_NAME: &str = "default"; + +/// The name of a group a task belongs to. +/// +/// This name is passed belong-side the task name to the prometheus metrics and can be used +/// to group tasks. +pub enum GroupName { + /// Sets the group name to `default`. + Default, + /// Use the specifically given name as group name. + Specific(&'static str), +} + +impl From> for GroupName { + fn from(name: Option<&'static str>) -> Self { + match name { + Some(name) => Self::Specific(name), + None => Self::Default, + } + } +} + +impl From<&'static str> for GroupName { + fn from(name: &'static str) -> Self { + Self::Specific(name) + } +} + +/// An handle for spawning tasks in the service. +#[derive(Clone)] +pub struct SpawnTaskHandle { + on_exit: exit_future::Exit, + tokio_handle: Handle, + metrics: Option, + task_registry: TaskRegistry, +} + +impl SpawnTaskHandle { + /// Spawns the given task with the given name and a group name. + /// If group is not specified `DEFAULT_GROUP_NAME` will be used. + /// + /// Note that the `name` is a `&'static str`. The reason for this choice is that + /// statistics about this task are getting reported to the Prometheus endpoint (if enabled), and + /// that therefore the set of possible task names must be bounded. + /// + /// In other words, it would be a bad idea for someone to do for example + /// `spawn(format!("{:?}", some_public_key))`. + pub fn spawn( + &self, + name: &'static str, + group: impl Into, + task: impl Future + Send + 'static, + ) { + self.spawn_inner(name, group, task, TaskType::Async) + } + + /// Spawns the blocking task with the given name. See also `spawn`. + pub fn spawn_blocking( + &self, + name: &'static str, + group: impl Into, + task: impl Future + Send + 'static, + ) { + self.spawn_inner(name, group, task, TaskType::Blocking) + } + + /// Helper function that implements the spawning logic. See `spawn` and `spawn_blocking`. + fn spawn_inner( + &self, + name: &'static str, + group: impl Into, + task: impl Future + Send + 'static, + task_type: TaskType, + ) { + let on_exit = self.on_exit.clone(); + let metrics = self.metrics.clone(); + let registry = self.task_registry.clone(); + + let group = match group.into() { + GroupName::Specific(var) => var, + // If no group is specified use default. + GroupName::Default => DEFAULT_GROUP_NAME, + }; + + let task_type_label = match task_type { + TaskType::Blocking => "blocking", + TaskType::Async => "async", + }; + + // Note that we increase the started counter here and not within the future. This way, + // we could properly visualize on Prometheus situations where the spawning doesn't work. + if let Some(metrics) = &self.metrics { + metrics + .tasks_spawned + .with_label_values(&[name, group, task_type_label]) + .inc(); + // We do a dummy increase in order for the task to show up in metrics. + metrics + .tasks_ended + .with_label_values(&[name, "finished", group, task_type_label]) + .inc_by(0); + } + + let future = async move { + // Register the task and keep the "token" alive until the task is ended. Then this + // "token" will unregister this task. + let _registry_token = registry.register_task(name, group); + + if let Some(metrics) = metrics { + // Add some wrappers around `task`. + let task = { + let poll_duration = + metrics + .poll_duration + .with_label_values(&[name, group, task_type_label]); + let poll_start = + metrics + .poll_start + .with_label_values(&[name, group, task_type_label]); + let inner = + prometheus_future::with_poll_durations(poll_duration, poll_start, task); + // The logic of `AssertUnwindSafe` here is ok considering that we throw + // away the `Future` after it has panicked. + panic::AssertUnwindSafe(inner).catch_unwind() + }; + futures::pin_mut!(task); + + match select(on_exit, task).await { + Either::Right((Err(payload), _)) => { + metrics + .tasks_ended + .with_label_values(&[name, "panic", group, task_type_label]) + .inc(); + panic::resume_unwind(payload) + } + Either::Right((Ok(()), _)) => { + metrics + .tasks_ended + .with_label_values(&[name, "finished", group, task_type_label]) + .inc(); + } + Either::Left(((), _)) => { + // The `on_exit` has triggered. + metrics + .tasks_ended + .with_label_values(&[name, "interrupted", group, task_type_label]) + .inc(); + } + } + } else { + futures::pin_mut!(task); + let _ = select(on_exit, task).await; + } + } + .in_current_span(); + + match task_type { + TaskType::Async => { + self.tokio_handle.spawn(future); + } + TaskType::Blocking => { + let handle = self.tokio_handle.clone(); + self.tokio_handle.spawn_blocking(move || { + handle.block_on(future); + }); + } + } + } +} + +impl sp_core::traits::SpawnNamed for SpawnTaskHandle { + fn spawn_blocking( + &self, + name: &'static str, + group: Option<&'static str>, + future: BoxFuture<'static, ()>, + ) { + self.spawn_inner(name, group, future, TaskType::Blocking) + } + + fn spawn( + &self, + name: &'static str, + group: Option<&'static str>, + future: BoxFuture<'static, ()>, + ) { + self.spawn_inner(name, group, future, TaskType::Async) + } +} + +/// A wrapper over `SpawnTaskHandle` that will notify a receiver whenever any +/// task spawned through it fails. The service should be on the receiver side +/// and will shut itself down whenever it receives any message, i.e. an +/// essential task has failed. +#[derive(Clone)] +pub struct SpawnEssentialTaskHandle { + essential_failed_tx: TracingUnboundedSender<()>, + inner: SpawnTaskHandle, +} + +impl SpawnEssentialTaskHandle { + /// Creates a new `SpawnEssentialTaskHandle`. + pub fn new( + essential_failed_tx: TracingUnboundedSender<()>, + spawn_task_handle: SpawnTaskHandle, + ) -> SpawnEssentialTaskHandle { + SpawnEssentialTaskHandle { + essential_failed_tx, + inner: spawn_task_handle, + } + } + + /// Spawns the given task with the given name. + /// + /// See also [`SpawnTaskHandle::spawn`]. + pub fn spawn( + &self, + name: &'static str, + group: impl Into, + task: impl Future + Send + 'static, + ) { + self.spawn_inner(name, group, task, TaskType::Async) + } + + /// Spawns the blocking task with the given name. + /// + /// See also [`SpawnTaskHandle::spawn_blocking`]. + pub fn spawn_blocking( + &self, + name: &'static str, + group: impl Into, + task: impl Future + Send + 'static, + ) { + self.spawn_inner(name, group, task, TaskType::Blocking) + } + + fn spawn_inner( + &self, + name: &'static str, + group: impl Into, + task: impl Future + Send + 'static, + task_type: TaskType, + ) { + let essential_failed = self.essential_failed_tx.clone(); + let essential_task = std::panic::AssertUnwindSafe(task) + .catch_unwind() + .map(move |_| { + log::error!("Essential task `{}` failed. Shutting down service.", name); + let _ = essential_failed.close(); + }); + + self.inner + .spawn_inner(name, group, essential_task, task_type); + } +} + +impl sp_core::traits::SpawnEssentialNamed for SpawnEssentialTaskHandle { + fn spawn_essential_blocking( + &self, + name: &'static str, + group: Option<&'static str>, + future: BoxFuture<'static, ()>, + ) { + self.spawn_blocking(name, group, future); + } + + fn spawn_essential( + &self, + name: &'static str, + group: Option<&'static str>, + future: BoxFuture<'static, ()>, + ) { + self.spawn(name, group, future); + } +} + +/// Helper struct to manage background/async tasks in Service. +pub struct TaskManager { + /// A future that resolves when the service has exited, this is useful to + /// make sure any internally spawned futures stop when the service does. + on_exit: exit_future::Exit, + /// A signal that makes the exit future above resolve, fired on drop. + _signal: Signal, + /// Tokio runtime handle that is used to spawn futures. + tokio_handle: Handle, + /// Prometheus metric where to report the polling times. + metrics: Option, + /// Send a signal when a spawned essential task has concluded. The next time + /// the service future is polled it should complete with an error. + essential_failed_tx: TracingUnboundedSender<()>, + /// A receiver for spawned essential-tasks concluding. + essential_failed_rx: TracingUnboundedReceiver<()>, + /// Things to keep alive until the task manager is dropped. + keep_alive: Box, + /// A list of other `TaskManager`'s to terminate and gracefully shutdown when the parent + /// terminates and gracefully shutdown. Also ends the parent `future()` if a child's essential + /// task fails. + children: Vec, + /// The registry of all running tasks. + task_registry: TaskRegistry, +} + +impl TaskManager { + /// If a Prometheus registry is passed, it will be used to report statistics about the + /// service tasks. + pub fn new( + tokio_handle: Handle, + prometheus_registry: Option<&Registry>, + ) -> Result { + let (signal, on_exit) = exit_future::signal(); + + // A side-channel for essential tasks to communicate shutdown. + let (essential_failed_tx, essential_failed_rx) = + tracing_unbounded("mpsc_essential_tasks", 100); + + let metrics = prometheus_registry.map(Metrics::register).transpose()?; + + Ok(Self { + on_exit, + _signal: signal, + tokio_handle, + metrics, + essential_failed_tx, + essential_failed_rx, + keep_alive: Box::new(()), + children: Vec::new(), + task_registry: Default::default(), + }) + } + + /// Get a handle for spawning tasks. + pub fn spawn_handle(&self) -> SpawnTaskHandle { + SpawnTaskHandle { + on_exit: self.on_exit.clone(), + tokio_handle: self.tokio_handle.clone(), + metrics: self.metrics.clone(), + task_registry: self.task_registry.clone(), + } + } + + /// Get a handle for spawning essential tasks. + pub fn spawn_essential_handle(&self) -> SpawnEssentialTaskHandle { + SpawnEssentialTaskHandle::new(self.essential_failed_tx.clone(), self.spawn_handle()) + } + + /// Return a future that will end with success if the signal to terminate was sent + /// (`self.terminate()`) or with an error if an essential task fails. + /// + /// # Warning + /// + /// This function will not wait until the end of the remaining task. + pub fn future<'a>( + &'a mut self, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + let mut t1 = self.essential_failed_rx.next().fuse(); + let mut t2 = self.on_exit.clone().fuse(); + let mut t3 = try_join_all( + self.children + .iter_mut() + .map(|x| x.future()) + // Never end this future if there is no error because if there is no children, + // it must not stop + .chain(std::iter::once(pending().boxed())), + ) + .fuse(); + + futures::select! { + _ = t1 => Err(Error::Other("Essential task failed.".into())), + _ = t2 => Ok(()), + res = t3 => Err(res.map(|_| ()).expect_err("this future never ends; qed")), + } + }) + } + + /// Set what the task manager should keep alive, can be called multiple times. + pub fn keep_alive(&mut self, to_keep_alive: T) { + // allows this fn to safely called multiple times. + use std::mem; + let old = mem::replace(&mut self.keep_alive, Box::new(())); + self.keep_alive = Box::new((to_keep_alive, old)); + } + + /// Register another TaskManager to terminate and gracefully shutdown when the parent + /// terminates and gracefully shutdown. Also ends the parent `future()` if a child's essential + /// task fails. (But don't end the parent if a child's normal task fails.) + pub fn add_child(&mut self, child: TaskManager) { + self.children.push(child); + } + + /// Consume `self` and return the [`TaskRegistry`]. + /// + /// This [`TaskRegistry`] can be used to check for still running tasks after this task manager + /// was dropped. + pub fn into_task_registry(self) -> TaskRegistry { + self.task_registry + } +} + +#[derive(Clone)] +struct Metrics { + // This list is ordered alphabetically + poll_duration: HistogramVec, + poll_start: CounterVec, + tasks_spawned: CounterVec, + tasks_ended: CounterVec, +} + +impl Metrics { + fn register(registry: &Registry) -> Result { + Ok(Self { + poll_duration: register(HistogramVec::new( + HistogramOpts { + common_opts: Opts::new( + "substrate_tasks_polling_duration", + "Duration in seconds of each invocation of Future::poll" + ), + buckets: exponential_buckets(0.001, 4.0, 9) + .expect("function parameters are constant and always valid; qed"), + }, + &["task_name", "task_group", "kind"] + )?, registry)?, + poll_start: register(CounterVec::new( + Opts::new( + "substrate_tasks_polling_started_total", + "Total number of times we started invoking Future::poll" + ), + &["task_name", "task_group", "kind"] + )?, registry)?, + tasks_spawned: register(CounterVec::new( + Opts::new( + "substrate_tasks_spawned_total", + "Total number of tasks that have been spawned on the Service" + ), + &["task_name", "task_group", "kind"] + )?, registry)?, + tasks_ended: register(CounterVec::new( + Opts::new( + "substrate_tasks_ended_total", + "Total number of tasks for which Future::poll has returned Ready(()) or panicked" + ), + &["task_name", "reason", "task_group", "kind"] + )?, registry)?, + }) + } +} + +/// Ensures that a [`Task`] is unregistered when this object is dropped. +struct UnregisterOnDrop { + task: Task, + registry: TaskRegistry, +} + +impl Drop for UnregisterOnDrop { + fn drop(&mut self) { + let mut tasks = self.registry.tasks.lock(); + + if let Entry::Occupied(mut entry) = (*tasks).entry(self.task.clone()) { + *entry.get_mut() -= 1; + + if *entry.get() == 0 { + entry.remove(); + } + } + } +} + +/// Represents a running async task in the [`TaskManager`]. +/// +/// As a task is identified by a name and a group, it is totally valid that there exists multiple +/// tasks with the same name and group. +#[derive(Clone, Hash, Eq, PartialEq)] +pub struct Task { + /// The name of the task. + pub name: &'static str, + /// The group this task is associated to. + pub group: &'static str, +} + +impl Task { + /// Returns if the `group` is the [`DEFAULT_GROUP_NAME`]. + pub fn is_default_group(&self) -> bool { + self.group == DEFAULT_GROUP_NAME + } +} + +/// Keeps track of all running [`Task`]s in [`TaskManager`]. +#[derive(Clone, Default)] +pub struct TaskRegistry { + tasks: Arc>>, +} + +impl TaskRegistry { + /// Register a task with the given `name` and `group`. + /// + /// Returns [`UnregisterOnDrop`] that ensures that the task is unregistered when this value is + /// dropped. + fn register_task(&self, name: &'static str, group: &'static str) -> UnregisterOnDrop { + let task = Task { name, group }; + + { + let mut tasks = self.tasks.lock(); + + *(*tasks).entry(task.clone()).or_default() += 1; + } + + UnregisterOnDrop { + task, + registry: self.clone(), + } + } + + /// Returns the running tasks. + /// + /// As a task is only identified by its `name` and `group`, there can be duplicate tasks. The + /// number per task represents the concurrently running tasks with the same identifier. + pub fn running_tasks(&self) -> HashMap { + (*self.tasks.lock()).clone() + } +} diff --git a/substrate/service/src/task_manager/prometheus_future.rs b/substrate/service/src/task_manager/prometheus_future.rs new file mode 100644 index 00000000000..bdca5342b0f --- /dev/null +++ b/substrate/service/src/task_manager/prometheus_future.rs @@ -0,0 +1,63 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Wrapper around a `Future` that reports statistics about when the `Future` is polled. + +use futures::prelude::*; +use prometheus_endpoint::{Counter, Histogram, U64}; +use std::{ + fmt, + pin::Pin, + task::{Context, Poll}, +}; + +/// Wraps around a `Future`. Report the polling duration to the `Histogram` and when the polling +/// starts to the `Counter`. +pub fn with_poll_durations( + poll_duration: Histogram, + poll_start: Counter, + inner: T, +) -> PrometheusFuture { + PrometheusFuture { + inner, + poll_duration, + poll_start, + } +} + +/// Wraps around `Future` and adds diagnostics to it. +#[pin_project::pin_project] +#[derive(Clone)] +pub struct PrometheusFuture { + /// The inner future doing the actual work. + #[pin] + inner: T, + poll_duration: Histogram, + poll_start: Counter, +} + +impl Future for PrometheusFuture +where + T: Future, +{ + type Output = T::Output; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let this = self.project(); + + this.poll_start.inc(); + let _timer = this.poll_duration.start_timer(); + Future::poll(this.inner, cx) + + // `_timer` is dropped here and will observe the duration + } +} + +impl fmt::Debug for PrometheusFuture +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&self.inner, f) + } +} diff --git a/substrate/service/src/task_manager/tests.rs b/substrate/service/src/task_manager/tests.rs new file mode 100644 index 00000000000..b18f351c3ae --- /dev/null +++ b/substrate/service/src/task_manager/tests.rs @@ -0,0 +1,240 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +use crate::task_manager::TaskManager; +use futures::{future::FutureExt, pin_mut, select}; +use parking_lot::Mutex; +use std::{any::Any, sync::Arc, time::Duration}; + +#[derive(Clone, Debug)] +struct DropTester(Arc>); + +struct DropTesterRef(DropTester); + +impl DropTester { + fn new() -> DropTester { + DropTester(Arc::new(Mutex::new(0))) + } + + fn new_ref(&self) -> DropTesterRef { + *self.0.lock() += 1; + DropTesterRef(self.clone()) + } + + fn wait_on_drop(&self) { + loop { + if *self.0.lock() == 0 { + break; + } + std::thread::sleep(std::time::Duration::from_millis(10)); + } + } +} + +impl PartialEq for DropTester { + fn eq(&self, other: &usize) -> bool { + &*self.0.lock() == other + } +} + +impl Drop for DropTesterRef { + fn drop(&mut self) { + *(self.0).0.lock() -= 1; + } +} + +#[test] +fn ensure_drop_tester_working() { + let drop_tester = DropTester::new(); + assert_eq!(drop_tester, 0); + let drop_tester_ref_1 = drop_tester.new_ref(); + assert_eq!(drop_tester, 1); + let drop_tester_ref_2 = drop_tester.new_ref(); + assert_eq!(drop_tester, 2); + drop(drop_tester_ref_1); + assert_eq!(drop_tester, 1); + drop(drop_tester_ref_2); + assert_eq!(drop_tester, 0); +} + +async fn run_background_task(_keep_alive: impl Any) { + loop { + tokio::time::sleep(Duration::from_secs(1)).await; + } +} + +async fn run_background_task_blocking(duration: Duration, _keep_alive: impl Any) { + loop { + // block for X sec (not interruptible) + std::thread::sleep(duration); + // await for 1 sec (interruptible) + tokio::time::sleep(Duration::from_secs(1)).await; + } +} + +fn new_task_manager(tokio_handle: tokio::runtime::Handle) -> TaskManager { + TaskManager::new(tokio_handle, None).unwrap() +} + +#[test] +fn ensure_tasks_are_awaited_on_shutdown() { + let drop_tester = DropTester::new(); + { + let runtime = tokio::runtime::Runtime::new().unwrap(); + let handle = runtime.handle().clone(); + + let task_manager = new_task_manager(handle); + let spawn_handle = task_manager.spawn_handle(); + spawn_handle.spawn("task1", None, run_background_task(drop_tester.new_ref())); + spawn_handle.spawn("task2", None, run_background_task(drop_tester.new_ref())); + assert_eq!(drop_tester, 2); + // allow the tasks to even start + runtime.block_on(async { tokio::time::sleep(Duration::from_secs(1)).await }); + assert_eq!(drop_tester, 2); + } + drop_tester.wait_on_drop(); +} + +#[test] +fn ensure_keep_alive_during_shutdown() { + let drop_tester = DropTester::new(); + { + let runtime = tokio::runtime::Runtime::new().unwrap(); + let handle = runtime.handle().clone(); + + let mut task_manager = new_task_manager(handle); + let spawn_handle = task_manager.spawn_handle(); + task_manager.keep_alive(drop_tester.new_ref()); + spawn_handle.spawn("task1", None, run_background_task(())); + assert_eq!(drop_tester, 1); + // allow the tasks to even start + runtime.block_on(async { tokio::time::sleep(Duration::from_secs(1)).await }); + assert_eq!(drop_tester, 1); + } + drop_tester.wait_on_drop(); +} + +#[test] +fn ensure_blocking_futures_are_awaited_on_shutdown() { + let drop_tester = DropTester::new(); + { + let runtime = tokio::runtime::Runtime::new().unwrap(); + let handle = runtime.handle().clone(); + + let task_manager = new_task_manager(handle); + let spawn_handle = task_manager.spawn_handle(); + spawn_handle.spawn( + "task1", + None, + run_background_task_blocking(Duration::from_secs(3), drop_tester.new_ref()), + ); + spawn_handle.spawn( + "task2", + None, + run_background_task_blocking(Duration::from_secs(3), drop_tester.new_ref()), + ); + assert_eq!(drop_tester, 2); + // allow the tasks to even start + runtime.block_on(async { tokio::time::sleep(Duration::from_secs(1)).await }); + assert_eq!(drop_tester, 2); + } + assert_eq!(drop_tester, 0); +} + +#[test] +fn ensure_task_manager_future_ends_with_error_when_essential_task_fails() { + let drop_tester = DropTester::new(); + { + let runtime = tokio::runtime::Runtime::new().unwrap(); + let handle = runtime.handle().clone(); + + let mut task_manager = new_task_manager(handle); + let spawn_handle = task_manager.spawn_handle(); + let spawn_essential_handle = task_manager.spawn_essential_handle(); + spawn_handle.spawn("task1", None, run_background_task(drop_tester.new_ref())); + spawn_handle.spawn("task2", None, run_background_task(drop_tester.new_ref())); + assert_eq!(drop_tester, 2); + // allow the tasks to even start + runtime.block_on(async { tokio::time::sleep(Duration::from_secs(1)).await }); + assert_eq!(drop_tester, 2); + spawn_essential_handle.spawn("task3", None, async { panic!("task failed") }); + runtime + .block_on(task_manager.future()) + .expect_err("future()'s Result must be Err"); + assert_eq!(drop_tester, 2); + } + drop_tester.wait_on_drop(); +} + +#[test] +fn ensure_task_manager_future_ends_with_error_when_childs_essential_task_fails() { + let drop_tester = DropTester::new(); + { + let runtime = tokio::runtime::Runtime::new().unwrap(); + let handle = runtime.handle().clone(); + + let mut task_manager = new_task_manager(handle.clone()); + let child_1 = new_task_manager(handle.clone()); + let spawn_handle_child_1 = child_1.spawn_handle(); + let spawn_essential_handle_child_1 = child_1.spawn_essential_handle(); + let child_2 = new_task_manager(handle.clone()); + let spawn_handle_child_2 = child_2.spawn_handle(); + task_manager.add_child(child_1); + task_manager.add_child(child_2); + let spawn_handle = task_manager.spawn_handle(); + spawn_handle.spawn("task1", None, run_background_task(drop_tester.new_ref())); + spawn_handle.spawn("task2", None, run_background_task(drop_tester.new_ref())); + spawn_handle_child_1.spawn("task3", None, run_background_task(drop_tester.new_ref())); + spawn_handle_child_2.spawn("task4", None, run_background_task(drop_tester.new_ref())); + assert_eq!(drop_tester, 4); + // allow the tasks to even start + runtime.block_on(async { tokio::time::sleep(Duration::from_secs(1)).await }); + assert_eq!(drop_tester, 4); + spawn_essential_handle_child_1.spawn("task5", None, async { panic!("task failed") }); + runtime + .block_on(task_manager.future()) + .expect_err("future()'s Result must be Err"); + assert_eq!(drop_tester, 4); + } + drop_tester.wait_on_drop(); +} + +#[test] +fn ensure_task_manager_future_continues_when_childs_not_essential_task_fails() { + let drop_tester = DropTester::new(); + { + let runtime = tokio::runtime::Runtime::new().unwrap(); + let handle = runtime.handle().clone(); + + let mut task_manager = new_task_manager(handle.clone()); + let child_1 = new_task_manager(handle.clone()); + let spawn_handle_child_1 = child_1.spawn_handle(); + let child_2 = new_task_manager(handle.clone()); + let spawn_handle_child_2 = child_2.spawn_handle(); + task_manager.add_child(child_1); + task_manager.add_child(child_2); + let spawn_handle = task_manager.spawn_handle(); + spawn_handle.spawn("task1", None, run_background_task(drop_tester.new_ref())); + spawn_handle.spawn("task2", None, run_background_task(drop_tester.new_ref())); + spawn_handle_child_1.spawn("task3", None, run_background_task(drop_tester.new_ref())); + spawn_handle_child_2.spawn("task4", None, run_background_task(drop_tester.new_ref())); + assert_eq!(drop_tester, 4); + // allow the tasks to even start + runtime.block_on(async { tokio::time::sleep(Duration::from_secs(1)).await }); + assert_eq!(drop_tester, 4); + spawn_handle_child_1.spawn("task5", None, async { panic!("task failed") }); + runtime.block_on(async { + let t1 = task_manager.future().fuse(); + let t2 = tokio::time::sleep(Duration::from_secs(3)).fuse(); + + pin_mut!(t1, t2); + + select! { + res = t1 => panic!("task should not have stopped: {:?}", res), + _ = t2 => {}, + } + }); + assert_eq!(drop_tester, 4); + } + drop_tester.wait_on_drop(); +} diff --git a/utils/gear-workspace-hack/Cargo.toml b/utils/gear-workspace-hack/Cargo.toml index f5ff2b2adbc..6603ad0320f 100644 --- a/utils/gear-workspace-hack/Cargo.toml +++ b/utils/gear-workspace-hack/Cargo.toml @@ -321,6 +321,7 @@ impl-serde = { version = "0.4" } indexmap = { version = "2", features = ["serde"] } ipnet = { version = "2" } itertools-582f2526e08bb6a0 = { package = "itertools", version = "0.14" } +itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13" } itertools-93f6ce9d446188ac = { package = "itertools", version = "0.10" } js-sys = { version = "0.3" } jsonrpsee = { version = "0.24", default-features = false, features = ["client", "macros", "server"] } @@ -604,6 +605,7 @@ impl-serde = { version = "0.4" } indexmap = { version = "2", features = ["serde"] } ipnet = { version = "2" } itertools-582f2526e08bb6a0 = { package = "itertools", version = "0.14" } +itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13" } itertools-93f6ce9d446188ac = { package = "itertools", version = "0.10" } js-sys = { version = "0.3" } jsonrpsee = { version = "0.24", default-features = false, features = ["client", "macros", "server"] } @@ -777,7 +779,6 @@ errno = { version = "0.3" } hickory-proto = { version = "0.25", default-features = false, features = ["mdns", "tokio"] } hyper-rustls = { version = "0.27", default-features = false, features = ["aws-lc-rs", "http1", "http2", "logging", "ring", "tls12", "webpki-tokio"] } hyper-util = { version = "0.1", default-features = false, features = ["client-proxy"] } -itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13" } libc = { version = "0.2", default-features = false, features = ["extra_traits"] } librocksdb-sys = { version = "0.11", features = ["jemalloc", "snappy"] } linux-raw-sys = { version = "0.11", default-features = false, features = ["auxvec", "elf", "errno", "general", "ioctl", "no_std", "prctl"] } @@ -801,7 +802,6 @@ errno = { version = "0.3" } hickory-proto = { version = "0.25", default-features = false, features = ["mdns", "tokio"] } hyper-rustls = { version = "0.27", default-features = false, features = ["aws-lc-rs", "http1", "http2", "logging", "ring", "tls12", "webpki-tokio"] } hyper-util = { version = "0.1", default-features = false, features = ["client-proxy"] } -itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13" } libc = { version = "0.2", default-features = false, features = ["extra_traits"] } librocksdb-sys = { version = "0.11", features = ["jemalloc", "snappy"] } linux-raw-sys = { version = "0.11", default-features = false, features = ["auxvec", "elf", "errno", "general", "ioctl", "no_std", "prctl"] } @@ -825,7 +825,6 @@ errno = { version = "0.3" } hickory-proto = { version = "0.25", default-features = false, features = ["mdns", "tokio"] } hyper-rustls = { version = "0.27", default-features = false, features = ["aws-lc-rs", "http1", "http2", "logging", "ring", "tls12", "webpki-tokio"] } hyper-util = { version = "0.1", default-features = false, features = ["client-proxy"] } -itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13" } libc = { version = "0.2", default-features = false, features = ["extra_traits"] } librocksdb-sys = { version = "0.11", features = ["jemalloc", "snappy"] } linux-raw-sys = { version = "0.11", default-features = false, features = ["auxvec", "elf", "errno", "general", "ioctl", "no_std", "prctl"] } @@ -849,7 +848,6 @@ errno = { version = "0.3" } hickory-proto = { version = "0.25", default-features = false, features = ["mdns", "tokio"] } hyper-rustls = { version = "0.27", default-features = false, features = ["aws-lc-rs", "http1", "http2", "logging", "ring", "tls12", "webpki-tokio"] } hyper-util = { version = "0.1", default-features = false, features = ["client-proxy"] } -itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13" } libc = { version = "0.2", default-features = false, features = ["extra_traits"] } librocksdb-sys = { version = "0.11", features = ["jemalloc", "snappy"] } linux-raw-sys = { version = "0.11", default-features = false, features = ["auxvec", "elf", "errno", "general", "ioctl", "no_std", "prctl"] } @@ -873,7 +871,6 @@ errno = { version = "0.3" } hickory-proto = { version = "0.25", default-features = false, features = ["mdns", "tokio"] } hyper-rustls = { version = "0.27", default-features = false, features = ["aws-lc-rs", "http1", "http2", "logging", "ring", "tls12", "webpki-tokio"] } hyper-util = { version = "0.1", default-features = false, features = ["client-proxy"] } -itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13" } libc = { version = "0.2", default-features = false, features = ["extra_traits"] } librocksdb-sys = { version = "0.11", features = ["jemalloc", "snappy"] } miniz_oxide = { version = "0.8", default-features = false, features = ["simd", "with-alloc"] } @@ -895,7 +892,6 @@ errno = { version = "0.3" } hickory-proto = { version = "0.25", default-features = false, features = ["mdns", "tokio"] } hyper-rustls = { version = "0.27", default-features = false, features = ["aws-lc-rs", "http1", "http2", "logging", "ring", "tls12", "webpki-tokio"] } hyper-util = { version = "0.1", default-features = false, features = ["client-proxy"] } -itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13" } libc = { version = "0.2", default-features = false, features = ["extra_traits"] } librocksdb-sys = { version = "0.11", features = ["jemalloc", "snappy"] } miniz_oxide = { version = "0.8", default-features = false, features = ["simd", "with-alloc"] }