From 70d748fb2b6ca9b233abf7d91eaacce14bdb925d Mon Sep 17 00:00:00 2001 From: Tobi Demeco <50408393+TDemeco@users.noreply.github.com> Date: Wed, 4 Mar 2026 07:56:06 -0300 Subject: [PATCH 1/2] =?UTF-8?q?build:=20=E2=AC=86=EF=B8=8F=20upgrade=20to?= =?UTF-8?q?=20StorageHub=20v0.4.3=20(#468)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New update to StorageHub containing only a simple bugfix. No changes required in DataHaven Co-authored-by: undercover-cactus --- operator/Cargo.lock | 148 ++++++++++++++++++++++---------------------- operator/Cargo.toml | 68 ++++++++++---------- 2 files changed, 108 insertions(+), 108 deletions(-) diff --git a/operator/Cargo.lock b/operator/Cargo.lock index 2843c578..586435c3 100644 --- a/operator/Cargo.lock +++ b/operator/Cargo.lock @@ -8642,8 +8642,8 @@ dependencies = [ [[package]] name = "pallet-bucket-nfts" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-benchmarking", "frame-support", @@ -8699,8 +8699,8 @@ dependencies = [ [[package]] name = "pallet-cr-randomness" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-support", "frame-system", @@ -8983,8 +8983,8 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-file-system" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "fp-account", "fp-evm", @@ -9222,8 +9222,8 @@ dependencies = [ [[package]] name = "pallet-file-system" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-benchmarking", "frame-support", @@ -9251,8 +9251,8 @@ dependencies = [ [[package]] name = "pallet-file-system-runtime-api" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "parity-scale-codec", "scale-info", @@ -9466,8 +9466,8 @@ dependencies = [ [[package]] name = "pallet-payment-streams" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-benchmarking", "frame-support", @@ -9486,8 +9486,8 @@ dependencies = [ [[package]] name = "pallet-payment-streams-runtime-api" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "parity-scale-codec", "scale-info", @@ -9514,8 +9514,8 @@ dependencies = [ [[package]] name = "pallet-proofs-dealer" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-benchmarking", "frame-support", @@ -9540,8 +9540,8 @@ dependencies = [ [[package]] name = "pallet-proofs-dealer-runtime-api" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "parity-scale-codec", "scale-info", @@ -9579,8 +9579,8 @@ dependencies = [ [[package]] name = "pallet-randomness" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-benchmarking", "frame-support", @@ -9717,8 +9717,8 @@ dependencies = [ [[package]] name = "pallet-storage-providers" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-benchmarking", "frame-support", @@ -9739,8 +9739,8 @@ dependencies = [ [[package]] name = "pallet-storage-providers-runtime-api" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "parity-scale-codec", "scale-info", @@ -13902,8 +13902,8 @@ dependencies = [ [[package]] name = "shc-actors-derive" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "once_cell", "proc-macro2", @@ -13915,8 +13915,8 @@ dependencies = [ [[package]] name = "shc-actors-framework" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "anyhow", "bincode", @@ -13934,8 +13934,8 @@ dependencies = [ [[package]] name = "shc-blockchain-service" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "anyhow", "array-bytes", @@ -13990,8 +13990,8 @@ dependencies = [ [[package]] name = "shc-blockchain-service-db" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "chrono", "diesel", @@ -14014,8 +14014,8 @@ dependencies = [ [[package]] name = "shc-client" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "anyhow", "array-bytes", @@ -14089,8 +14089,8 @@ dependencies = [ [[package]] name = "shc-common" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "anyhow", "bigdecimal", @@ -14154,8 +14154,8 @@ dependencies = [ [[package]] name = "shc-file-manager" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "bincode", "hash-db", @@ -14179,8 +14179,8 @@ dependencies = [ [[package]] name = "shc-file-transfer-service" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "anyhow", "array-bytes", @@ -14210,8 +14210,8 @@ dependencies = [ [[package]] name = "shc-fisherman-service" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "async-trait", "diesel", @@ -14241,8 +14241,8 @@ dependencies = [ [[package]] name = "shc-forest-manager" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "anyhow", "async-trait", @@ -14267,8 +14267,8 @@ dependencies = [ [[package]] name = "shc-indexer-db" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "bigdecimal", "chrono", @@ -14295,8 +14295,8 @@ dependencies = [ [[package]] name = "shc-indexer-service" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "anyhow", "array-bytes", @@ -14346,8 +14346,8 @@ dependencies = [ [[package]] name = "shc-rpc" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "array-bytes", "async-trait", @@ -14392,8 +14392,8 @@ dependencies = [ [[package]] name = "shc-telemetry" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "log", "substrate-prometheus-endpoint", @@ -14409,8 +14409,8 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "shp-constants" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "sp-core", "sp-runtime", @@ -14418,8 +14418,8 @@ dependencies = [ [[package]] name = "shp-data-price-updater" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-support", "parity-scale-codec", @@ -14433,8 +14433,8 @@ dependencies = [ [[package]] name = "shp-file-key-verifier" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-support", "parity-scale-codec", @@ -14451,8 +14451,8 @@ dependencies = [ [[package]] name = "shp-file-metadata" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "hex", "num-bigint", @@ -14467,8 +14467,8 @@ dependencies = [ [[package]] name = "shp-forest-verifier" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-support", "parity-scale-codec", @@ -14484,16 +14484,16 @@ dependencies = [ [[package]] name = "shp-opaque" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "sp-runtime", ] [[package]] name = "shp-session-keys" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "async-trait", "parity-scale-codec", @@ -14507,8 +14507,8 @@ dependencies = [ [[package]] name = "shp-traits" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-support", "parity-scale-codec", @@ -14521,8 +14521,8 @@ dependencies = [ [[package]] name = "shp-treasury-funding" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "log", "shp-traits", @@ -14532,8 +14532,8 @@ dependencies = [ [[package]] name = "shp-tx-implicits-runtime-api" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "parity-scale-codec", "scale-info", @@ -14545,8 +14545,8 @@ dependencies = [ [[package]] name = "shp-types" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "sp-core", "sp-runtime", diff --git a/operator/Cargo.toml b/operator/Cargo.toml index ab8b6185..40a689e9 100644 --- a/operator/Cargo.toml +++ b/operator/Cargo.toml @@ -272,42 +272,42 @@ fc-storage = { git = "https://github.com/polkadot-evm/frontier", branch = "stabl # StorageHub ## Runtime -pallet-bucket-nfts = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-cr-randomness = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-file-system = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-file-system-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-payment-streams = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-payment-streams-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-proofs-dealer = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-proofs-dealer-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-randomness = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-storage-providers = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-storage-providers-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-constants = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-data-price-updater = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-file-key-verifier = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-file-metadata = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-forest-verifier = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-traits = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-treasury-funding = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } +pallet-bucket-nfts = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-cr-randomness = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-file-system = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-file-system-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-payment-streams = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-payment-streams-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-proofs-dealer = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-proofs-dealer-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-randomness = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-storage-providers = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-storage-providers-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-constants = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-data-price-updater = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-file-key-verifier = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-file-metadata = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-forest-verifier = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-traits = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-treasury-funding = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } ## Client -shc-actors-derive = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-actors-framework = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-blockchain-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-client = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-common = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-file-manager = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-file-transfer-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-fisherman-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-forest-manager = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-indexer-db = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-indexer-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-rpc = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-opaque = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-tx-implicits-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-types = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } +shc-actors-derive = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-actors-framework = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-blockchain-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-client = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-common = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-file-manager = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-file-transfer-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-fisherman-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-forest-manager = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-indexer-db = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-indexer-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-rpc = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-opaque = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-tx-implicits-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-types = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } ## Precompiles -pallet-evm-precompile-file-system = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } +pallet-evm-precompile-file-system = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } # Static linking From aa3409b239312ec2a5c984539dc83b552846c5b0 Mon Sep 17 00:00:00 2001 From: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:25:17 +0100 Subject: [PATCH 2/2] feat(slashes): typed offence kinds, Perbill-to-WAD conversion, historical filtering, and liveness E2E test (#447) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Introduces typed offence classification, a linear Perbill-to-WAD conversion for EigenLayer slashing, historical offence filtering, and a new E2E test proving end-to-end liveness detection through `pallet_im_online`. --- ### OffenceKind enum New `OffenceKind` enum classifies consensus offences: - `LivenessOffence` — missed heartbeats (ImOnline) - `BabeEquivocation` — double block production - `GrandpaEquivocation` — double finality votes - `BeefyEquivocation` — double BEEFY votes / fork voting / future block voting - `Custom(BoundedVec)` — manual / governance slashes Each variant carries a human-readable description string through the Snowbridge message to EigenLayer's `DatahavenServiceManager.slashValidatorsOperator()`. ### EquivocationReportWrapper Generic wrapper around `ReportOffence` wired for BABE, GRANDPA, BEEFY, and ImOnline in all three runtimes: 1. **Filters historical offences** — discards reports whose session predates the bonding period, using `BondedEras` storage (analogous to `FilterHistoricalOffences` in `pallet_staking`, but adapted to this pallet's own era tracking). 2. **Tags offence kind** — stores the `OffenceKind` in `PendingOffenceKind` double-map `(SessionIndex, ValidatorId)` before delegating to `pallet_offences`. The `on_offence` handler reads it via `take()` in the same block. 3. **Cleans up on failure** — removes stale `PendingOffenceKind` entries if the inner reporter returns an error (e.g. duplicate report), preventing them from leaking into unrelated future offences. ### Perbill to WAD conversion and MaxSlashWad #### How Substrate computes slash fractions Each offence type in Substrate defines its own `slash_fraction(offenders_count)` returning a `Perbill`: | Offence | Formula | Typical range | |---|---|---| | **BABE equivocation** | `min((3k/n)^2, 1)` | 1 offender / 100 validators: ~0.09%; 1/2: capped to 100% | | **GRANDPA equivocation** | `min((3k/n)^2, 1)` | Same as BABE | | **BEEFY double-vote** | `min((3k/n)^2, 1)` | Same as BABE/GRANDPA | | **BEEFY fork/future voting** | Fixed `50%` | Always 50% | | **ImOnline liveness** | `min(3*(k - floor(n/10) - 1)/n, 1) * 7%` | 10% or fewer offline: **0%**; ~33% offline: ~5%; ~43% offline: 7% (max) | Where `k` = number of concurrent offenders, `n` = validator set size. **Key behavior for small validator sets (E2E):** With n=2, the ImOnline threshold is `floor(2/10) + 1 = 1`. A single offender (`k=1`) fails `checked_sub(1)` giving `Perbill(0)`. This means no `Slashes` storage entry is created (since `compute_slash` returns `None` when the new fraction doesn't exceed the prior slash), but the `SlashReported` event is still emitted, proving the full detection pipeline works. #### Linear conversion to EigenLayer WAD The Substrate `Perbill` is linearly mapped to a WAD value capped by `MaxSlashWad`: ``` WAD = perbill.deconstruct() * MaxSlashWad / 1_000_000_000 ``` - `MaxSlashWad` default: **5e16** (= 5% in WAD format, where 1e18 = 100%) - Governance-changeable dynamic runtime parameter (codec index 46) - `Perbill(100%)` maps to exactly `MaxSlashWad` (the cap) - `Perbill(0%)` maps to 0 (no slash sent to EigenLayer) #### Concrete examples (with default MaxSlashWad = 5%) | Scenario | Substrate Perbill | WAD sent to EigenLayer | EigenLayer % | |---|---|---|---| | BABE equivocation (1 of 100 validators) | ~0.09% | ~4.5e13 | ~0.0045% | | BABE equivocation (1 of 2 validators) | 100% (capped) | 5e16 | 5% (max) | | BEEFY fork voting | 50% | 2.5e16 | 2.5% | | ImOnline liveness (1 of 2 offline) | 0% | 0 (no slash) | 0% | | ImOnline liveness (~33% of large set offline) | ~5% | ~2.5e15 | ~0.25% | | Manual `force_inject_slash` at 20% | 20% | 1e16 | 1% | | Manual `force_inject_slash` at 100% | 100% | 5e16 | 5% (max) | The same WAD value is applied uniformly to all configured strategies via the `SlashingRequest` struct sent through Snowbridge to `DatahavenServiceManager.slashValidatorsOperator()`. ### E2E liveness slashing test New test scenario (`should detect and slash an unresponsive validator`) validates the full liveness detection pipeline: 1. Pauses bob's Docker container (preserving GRANDPA state via `docker pause`) 2. Waits 200s (>= 2 full sessions) for `pallet_im_online` to detect missed heartbeats 3. Unpauses bob to restore GRANDPA finality (2/2 validators needed) 4. Polls for `SlashReported` event (not `Slashes` storage — see slash fraction note above) 5. Verifies the event confirms the full pipeline: `pallet_im_online -> EquivocationReportWrapper -> pallet_offences -> on_offence` The test uses `try/finally` to always unpause bob, `{ at: "best" }` queries for non-finalized chain state during the pause, and drains prior `SlashReported` events before starting. ### Tests - **10 new unit tests**: `PendingOffenceKind` double-map semantics, session isolation, wrapper historical filtering, error cleanup, WAD conversion (100%, 50%, 0%), offence kind description propagation - **New mock infrastructure**: `MockInnerReporter`, `MockOffence`, `MockOkOutboundQueue` with slash data capture - **E2E**: Updated `force_inject_slash` test to use `offence_kind` enum, new liveness detection test --------- Co-authored-by: Claude Opus 4.6 Co-authored-by: Gonza Montiel Co-authored-by: undercover-cactus --- .../src/benchmarking.rs | 26 +- .../external-validator-slashes/src/lib.rs | 229 +++++++++- .../external-validator-slashes/src/mock.rs | 84 +++- .../external-validator-slashes/src/tests.rs | 402 +++++++++++++++++- .../runtime/common/src/slashes_adapter.rs | 4 +- operator/runtime/mainnet/src/configs/mod.rs | 37 +- .../mainnet/src/configs/runtime_params.rs | 10 + operator/runtime/stagenet/src/configs/mod.rs | 37 +- .../stagenet/src/configs/runtime_params.rs | 10 + operator/runtime/testnet/src/configs/mod.rs | 37 +- .../testnet/src/configs/runtime_params.rs | 10 + test/.papi/descriptors/package.json | 2 +- test/.papi/metadata/datahaven.scale | Bin 631232 -> 632706 bytes test/e2e/suites/slash.test.ts | 116 ++++- 14 files changed, 936 insertions(+), 68 deletions(-) diff --git a/operator/pallets/external-validator-slashes/src/benchmarking.rs b/operator/pallets/external-validator-slashes/src/benchmarking.rs index 51693fb7..7e8d7a00 100644 --- a/operator/pallets/external-validator-slashes/src/benchmarking.rs +++ b/operator/pallets/external-validator-slashes/src/benchmarking.rs @@ -41,7 +41,14 @@ mod benchmarks { let era = T::EraIndexProvider::active_era().index; let dummy = || T::AccountId::decode(&mut TrailingZeroInput::zeroes()).unwrap(); for _ in 0..MAX_SLASHES { - existing_slashes.push(Slash::::default_from(dummy())); + existing_slashes.push(Slash { + validator: dummy(), + reporters: vec![], + slash_id: One::one(), + percentage: Perbill::from_percent(1), + confirmed: false, + offence_kind: OffenceKind::LivenessOffence, + }); } Slashes::::insert( era.saturating_add(T::SlashDeferDuration::get()) @@ -74,7 +81,13 @@ mod benchmarks { let era = T::EraIndexProvider::active_era().index; let dummy = || T::AccountId::decode(&mut TrailingZeroInput::zeroes()).unwrap(); #[extrinsic_call] - _(RawOrigin::Root, era, dummy(), Perbill::from_percent(50)); + _( + RawOrigin::Root, + era, + dummy(), + Perbill::from_percent(50), + OffenceKind::LivenessOffence, + ); assert_eq!( Slashes::::get( @@ -93,7 +106,14 @@ mod benchmarks { let dummy = || T::AccountId::decode(&mut TrailingZeroInput::zeroes()).unwrap(); for _ in 0..(s + 1) { - queue.push_back(Slash::::default_from(dummy())); + queue.push_back(Slash { + validator: dummy(), + reporters: vec![], + slash_id: One::one(), + percentage: Perbill::from_percent(1), + confirmed: false, + offence_kind: OffenceKind::LivenessOffence, + }); } UnreportedSlashesQueue::::set(queue); diff --git a/operator/pallets/external-validator-slashes/src/lib.rs b/operator/pallets/external-validator-slashes/src/lib.rs index 8ef718d3..b24b85db 100644 --- a/operator/pallets/external-validator-slashes/src/lib.rs +++ b/operator/pallets/external-validator-slashes/src/lib.rs @@ -31,7 +31,7 @@ extern crate alloc; use pallet_external_validators::apply; use snowbridge_outbound_queue_primitives::SendError; use { - alloc::{collections::vec_deque::VecDeque, vec, vec::Vec}, + alloc::{collections::vec_deque::VecDeque, string::String, vec, vec::Vec}, frame_support::{pallet_prelude::*, traits::DefensiveSaturating}, frame_system::pallet_prelude::*, log::log, @@ -46,7 +46,7 @@ use { DispatchResult, Perbill, }, sp_staking::{ - offence::{OffenceDetails, OnOffenceHandler}, + offence::{Offence, OffenceDetails, OffenceError, OnOffenceHandler, ReportOffence}, EraIndex, SessionIndex, }, }; @@ -63,10 +63,45 @@ mod tests; mod benchmarking; pub mod weights; +/// Identifies the type of consensus offence for EigenLayer slash reporting. +#[derive( + Encode, + Decode, + DecodeWithMemTracking, + RuntimeDebug, + TypeInfo, + Clone, + PartialEq, + Eq, + MaxEncodedLen, +)] +pub enum OffenceKind { + /// Liveness offence (i.e. Unresponsiveness) + LivenessOffence, + BabeEquivocation, + GrandpaEquivocation, + BeefyEquivocation, + Custom(BoundedVec>), +} + +impl OffenceKind { + pub fn to_description(&self) -> String { + match self { + Self::LivenessOffence => "Liveness offence".into(), + Self::BabeEquivocation => "BABE equivocation".into(), + Self::GrandpaEquivocation => "GRANDPA equivocation".into(), + Self::BeefyEquivocation => "BEEFY equivocation".into(), + Self::Custom(desc) => String::from_utf8(desc.to_vec()) + .unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned()), + } + } +} + #[derive(Debug, PartialEq, Eq, Clone)] pub struct SlashData { pub validator: AccountId, pub wad_to_slash: u128, + pub description: String, } // FIXME (nice to have): Merge with SendMessage trait from pallet external-validator-reward (similar trait) @@ -153,6 +188,11 @@ pub mod pallet { /// Provider to retrieve the current external index of validators type ExternalIndexProvider: ExternalIndexProvider; + /// Maximum WAD value for EigenLayer slashing. Maps Perbill(100%) to this value. + /// Default: 5e16 = 5% in WAD format (1e18 = 100%). + #[pallet::constant] + type MaxSlashWad: Get; + /// How many queued slashes are being processed per block. #[pallet::constant] type QueuedSlashesProcessedPerBlock: Get; @@ -183,6 +223,9 @@ pub mod pallet { EthereumDeliverFail, /// Invalid params for root_test_send_msg_to_eth RootTestInvalidParams, + /// No PendingOffenceKind found for (session, validator) — offence was not + /// reported through EquivocationReportWrapper, so the offence kind is unknown. + MissingOffenceKind, } #[apply(derive_storage_traits)] @@ -237,6 +280,27 @@ pub mod pallet { #[pallet::storage] pub type SlashingMode = StorageValue<_, SlashingModeOption, ValueQuery>; + /// Temporarily stores the offence kind per (session, offender), set by + /// `EquivocationReportWrapper` before `on_offence` is called synchronously within + /// the same block. Keyed by session index and validator ID so that offences from + /// different sessions or for different validators cannot interfere with each other. + /// + /// SAFETY: relies on `pallet_offences::report_offence` calling `on_offence` + /// synchronously in the same block. Entries are cleaned up via `take()` in + /// `on_offence` on success, or explicit `remove()` in the wrapper on error. + /// If the offence pipeline ever becomes asynchronous, this storage should be + /// replaced with an offence-payload-based approach. + #[pallet::storage] + pub type PendingOffenceKind = StorageDoubleMap< + _, + Twox64Concat, + SessionIndex, + Twox64Concat, + T::ValidatorId, + OffenceKind, + OptionQuery, + >; + #[pallet::genesis_config] #[derive(frame_support::DefaultNoBound)] pub struct GenesisConfig { @@ -305,6 +369,7 @@ pub mod pallet { era: EraIndex, validator: T::AccountId, percentage: Perbill, + offence_kind: OffenceKind, ) -> DispatchResult { ensure_root(origin)?; let active_era = T::EraIndexProvider::active_era().index; @@ -324,6 +389,7 @@ pub mod pallet { era, validator, slash_defer_duration, + offence_kind, ) .ok_or(Error::::ErrorComputingSlash)?; @@ -374,7 +440,8 @@ pub mod pallet { } } -/// This is intended to be used with `FilterHistoricalOffences`. +/// This is intended to be used with `EquivocationReportWrapper`, which filters +/// out historical offences (before the bonding period) and tags the offence kind. impl OnOffenceHandler, Weight> for Pallet @@ -453,6 +520,25 @@ where for (details, slash_fraction) in offenders.iter().zip(slash_fraction) { let (stash, _) = &details.offender; + // Read the per-(session, offender) offence kind set by EquivocationReportWrapper. + // This is set synchronously before on_offence is called, so take() clears it. + // Type safety: `stash` is T::ValidatorId (from IdentificationTuple), matching + // the key used by the wrapper. The trait bounds above enforce ValidatorId == AccountId. + let offence_kind = match pallet::PendingOffenceKind::::take(slash_session, stash) { + Some(kind) => kind, + None => { + log!( + log::Level::Error, + "MissingOffenceKind for session {:?}, validator {:?} — skipping slash", + slash_session, + stash, + ); + add_db_reads_writes(1, 1); + continue; + } + }; + add_db_reads_writes(1, 1); + // Skip if the validator is invulnerable. if invulnerables.contains(stash) { continue; @@ -477,6 +563,7 @@ where slash_era, stash.clone(), slash_defer_duration, + offence_kind.clone(), ); if let Some(mut slash) = slash { @@ -594,9 +681,22 @@ impl Pallet { break; }; + // Convert Perbill to EigenLayer WAD format with linear mapping. + // Perbill(100%) → MaxSlashWad (e.g. 5% WAD = 5e16). + // Formula: perbill_inner * MaxSlashWad / 1e9 + // Clamp to MaxSlashWad to guard against overflow if governance + // sets MaxSlashWad high enough for saturating_mul to hit u128::MAX. + let max_wad = T::MaxSlashWad::get(); + let wad_to_slash = (slash.percentage.deconstruct() as u128) + .saturating_mul(max_wad) + .checked_div(1_000_000_000u128) + .unwrap_or(0) + .min(max_wad); + slashes_to_send.push(SlashData { validator: slash.validator, - wad_to_slash: u128::from_str_radix("10000000000000000", 10).unwrap(), // TODO: need to compute how much we slash (for now it is 1e16) + wad_to_slash, + description: slash.offence_kind.to_description(), }); } }); @@ -655,19 +755,8 @@ pub struct Slash { pub percentage: Perbill, // Whether the slash is confirmed or still needs to go through deferred period pub confirmed: bool, -} - -impl Slash { - /// Initializes the default object using the given `validator`. - pub fn default_from(validator: AccountId) -> Self { - Self { - validator, - reporters: vec![], - slash_id: One::one(), - percentage: Perbill::from_percent(50), - confirmed: false, - } - } + /// The type of consensus offence (relayed to EigenLayer as a description string). + pub offence_kind: OffenceKind, } /// Computes a slash of a validator and nominators. It returns an unapplied @@ -682,6 +771,7 @@ pub(crate) fn compute_slash( slash_era: EraIndex, stash: T::AccountId, slash_defer_duration: EraIndex, + offence_kind: OffenceKind, ) -> Option> { let prior_slash_p = ValidatorSlashInEra::::get(slash_era, &stash).unwrap_or(Zero::zero()); @@ -707,6 +797,7 @@ pub(crate) fn compute_slash( slash_id, reporters: Vec::new(), confirmed, + offence_kind, }) } @@ -714,3 +805,107 @@ pub(crate) fn compute_slash( fn is_sorted_and_unique(list: &[u32]) -> bool { list.windows(2).all(|w| w[0] < w[1]) } + +/// Trait for associating an `OffenceKind` with a reporter type. +pub trait OffenceKindProvider { + fn kind() -> OffenceKind; +} + +/// Extracts the validator (account) ID from an offender identification tuple. +pub trait HasValidatorId { + fn validator_id(&self) -> &ValidatorId; +} + +impl HasValidatorId for (V, F) { + fn validator_id(&self) -> &V { + &self.0 + } +} + +/// Wraps a `ReportOffence` implementation to: +/// 1. **Filter historical offences**: discard reports whose session predates the bonding +/// period (similar to `FilterHistoricalOffences` in `pallet_staking`, but using this +/// pallet's own `BondedEras` storage instead of staking eras). +/// 2. **Tag offence kind**: store the `OffenceKind` per offender in `PendingOffenceKind` +/// before delegating to the inner reporter, so that `on_offence` can read it via +/// `PendingOffenceKind::take()`. +/// +/// If the inner `report_offence` fails (e.g. duplicate report), stale `PendingOffenceKind` +/// entries are cleaned up to prevent leaking into unrelated future offences. +pub struct EquivocationReportWrapper(PhantomData<(T, Inner, Kind)>); + +impl ReportOffence for EquivocationReportWrapper +where + T: Config, + Inner: ReportOffence, + O: Offence, + Kind: OffenceKindProvider, + Id: HasValidatorId, +{ + fn report_offence(reporters: Vec, offence: O) -> Result<(), OffenceError> { + // Discard offences from before the bonding period. + let offence_session = offence.session_index(); + let bonded_eras = pallet::BondedEras::::get(); + if bonded_eras + .first() + .filter(|(_, start, _)| offence_session >= *start) + .is_none() + { + log!( + log::Level::Debug, + "discarding offence from session {} — predates bonded eras {:?}", + offence_session, + bonded_eras.first(), + ); + return Ok(()); + } + + let offenders = offence.offenders(); + for offender in &offenders { + pallet::PendingOffenceKind::::insert( + offence_session, + offender.validator_id(), + Kind::kind(), + ); + } + let result = Inner::report_offence(reporters, offence); + if result.is_err() { + for offender in &offenders { + pallet::PendingOffenceKind::::remove(offence_session, offender.validator_id()); + } + } + result + } + + fn is_known_offence(offenders: &[Id], time_slot: &O::TimeSlot) -> bool { + Inner::is_known_offence(offenders, time_slot) + } +} + +pub struct BabeEquivocation; +impl OffenceKindProvider for BabeEquivocation { + fn kind() -> OffenceKind { + OffenceKind::BabeEquivocation + } +} + +pub struct GrandpaEquivocation; +impl OffenceKindProvider for GrandpaEquivocation { + fn kind() -> OffenceKind { + OffenceKind::GrandpaEquivocation + } +} + +pub struct BeefyEquivocation; +impl OffenceKindProvider for BeefyEquivocation { + fn kind() -> OffenceKind { + OffenceKind::BeefyEquivocation + } +} + +pub struct ImOnlineUnresponsive; +impl OffenceKindProvider for ImOnlineUnresponsive { + fn kind() -> OffenceKind { + OffenceKind::LivenessOffence + } +} diff --git a/operator/pallets/external-validator-slashes/src/mock.rs b/operator/pallets/external-validator-slashes/src/mock.rs index c21f13c5..efe2c509 100644 --- a/operator/pallets/external-validator-slashes/src/mock.rs +++ b/operator/pallets/external-validator-slashes/src/mock.rs @@ -25,7 +25,7 @@ use { core::cell::RefCell, frame_support::{ parameter_types, - traits::{ConstU16, ConstU32, ConstU64, Get}, + traits::{ConstU128, ConstU16, ConstU32, ConstU64, Get}, weights::constants::RocksDbWeight, }, frame_system as system, @@ -132,7 +132,9 @@ thread_local! { pub static ERA_INDEX: RefCell = const { RefCell::new(0) }; pub static DEFER_PERIOD: RefCell = const { RefCell::new(2) }; pub static SENT_ETHEREUM_MESSAGE_NONCE: RefCell = const { RefCell::new(0) }; - + pub static MOCK_REPORT_OFFENCE_SHOULD_FAIL: RefCell = const { RefCell::new(false) }; + pub static MOCK_REPORT_OFFENCE_CALLED: RefCell = const { RefCell::new(false) }; + pub static LAST_SENT_SLASHES: RefCell>> = RefCell::new(Vec::new()); } impl MockEraIndexProvider { @@ -215,10 +217,16 @@ impl DeferPeriodGetter { } pub struct MockOkOutboundQueue; +impl MockOkOutboundQueue { + pub fn last_sent_slashes() -> Vec> { + LAST_SENT_SLASHES.with(|r| r.borrow().clone()) + } +} impl crate::SendMessage for MockOkOutboundQueue { type Ticket = (); type Message = (); - fn build(_: &Vec>, _: u32) -> Option { + fn build(slashes: &Vec>, _: u32) -> Option { + LAST_SENT_SLASHES.with(|r| *r.borrow_mut() = slashes.clone()); Some(()) } fn validate(_: Self::Ticket) -> Result { @@ -258,6 +266,7 @@ impl external_validator_slashes::Config for Test { type EraIndexProvider = MockEraIndexProvider; type InvulnerablesProvider = MockInvulnerableProvider; type ExternalIndexProvider = TimestampProvider; + type MaxSlashWad = ConstU128<50_000_000_000_000_000>; type QueuedSlashesProcessedPerBlock = ConstU32<20>; type WeightInfo = (); type SendMessage = MockOkOutboundQueue; @@ -289,6 +298,75 @@ impl sp_runtime::traits::Convert> for IdentityValidator { } } +// --- Mock infrastructure for testing EquivocationReportWrapper --- + +use sp_staking::offence::{Offence, OffenceError, ReportOffence}; + +/// A mock inner ReportOffence that can be configured to succeed or fail. +pub struct MockInnerReporter; + +impl MockInnerReporter { + pub fn set_should_fail(fail: bool) { + MOCK_REPORT_OFFENCE_SHOULD_FAIL.with(|r| *r.borrow_mut() = fail); + } + pub fn was_called() -> bool { + MOCK_REPORT_OFFENCE_CALLED.with(|r| *r.borrow()) + } + pub fn reset() { + MOCK_REPORT_OFFENCE_SHOULD_FAIL.with(|r| *r.borrow_mut() = false); + MOCK_REPORT_OFFENCE_CALLED.with(|r| *r.borrow_mut() = false); + } +} + +impl> ReportOffence for MockInnerReporter { + fn report_offence(_reporters: Vec, _offence: O) -> Result<(), OffenceError> { + MOCK_REPORT_OFFENCE_CALLED.with(|r| *r.borrow_mut() = true); + if MOCK_REPORT_OFFENCE_SHOULD_FAIL.with(|r| *r.borrow()) { + Err(OffenceError::DuplicateReport) + } else { + Ok(()) + } + } + fn is_known_offence(_offenders: &[Id], _time_slot: &O::TimeSlot) -> bool { + false + } +} + +/// A minimal mock Offence for testing the wrapper. +pub struct MockOffence { + pub session_index: SessionIndex, + pub offenders: Vec<(u64, ())>, +} + +impl Offence<(u64, ())> for MockOffence { + const ID: sp_staking::offence::Kind = *b"mock:offence0000"; + type TimeSlot = u128; + + fn offenders(&self) -> Vec<(u64, ())> { + self.offenders.clone() + } + fn session_index(&self) -> SessionIndex { + self.session_index + } + fn validator_set_count(&self) -> u32 { + 3 + } + fn time_slot(&self) -> Self::TimeSlot { + self.session_index as u128 + } + fn slash_fraction(&self, _offenders_count: u32) -> sp_runtime::Perbill { + sp_runtime::Perbill::from_percent(50) + } +} + +/// Type alias for the wrapper using the mock reporter with BabeEquivocation kind. +pub type MockBabeWrapper = + crate::EquivocationReportWrapper; + +/// Type alias for the wrapper using the mock reporter with GrandpaEquivocation kind. +pub type MockGrandpaWrapper = + crate::EquivocationReportWrapper; + pub fn run_block() { run_to_block(System::block_number() + 1); } diff --git a/operator/pallets/external-validator-slashes/src/tests.rs b/operator/pallets/external-validator-slashes/src/tests.rs index 0c21466c..126485b8 100644 --- a/operator/pallets/external-validator-slashes/src/tests.rs +++ b/operator/pallets/external-validator-slashes/src/tests.rs @@ -18,12 +18,14 @@ use { super::*, crate::{ mock::{ - new_test_ext, run_block, DeferPeriodGetter, ExternalValidatorSlashes, - MockEraIndexProvider, RuntimeEvent, RuntimeOrigin, System, Test, + new_test_ext, run_block, DeferPeriodGetter, ExternalValidatorSlashes, MockBabeWrapper, + MockEraIndexProvider, MockGrandpaWrapper, MockInnerReporter, MockOffence, + MockOkOutboundQueue, RuntimeEvent, RuntimeOrigin, System, Test, }, - Slash, + OffenceKind, Slash, }, - frame_support::{assert_noop, assert_ok}, + frame_support::{assert_noop, assert_ok, BoundedVec}, + sp_staking::offence::ReportOffence, }; #[test] @@ -35,6 +37,7 @@ fn root_can_inject_manual_offence() { 0, 1u64, Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_eq!( Slashes::::get(get_slashing_era(0)), @@ -43,7 +46,10 @@ fn root_can_inject_manual_offence() { percentage: Perbill::from_percent(75), confirmed: false, reporters: vec![], - slash_id: 0 + slash_id: 0, + offence_kind: OffenceKind::Custom(BoundedVec::truncate_from( + b"Test slash".to_vec() + )), }] ); assert_eq!(NextSlashId::::get(), 1); @@ -59,7 +65,8 @@ fn cannot_inject_future_era_offence() { RuntimeOrigin::root(), 1, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), ), Error::::ProvidedFutureEra ); @@ -76,7 +83,8 @@ fn cannot_inject_era_offence_too_far_in_the_past() { RuntimeOrigin::root(), 1, 4u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), ), Error::::ProvidedNonSlashableEra ); @@ -91,7 +99,8 @@ fn root_can_cancel_deferred_slash() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_ok!(ExternalValidatorSlashes::cancel_deferred_slash( RuntimeOrigin::root(), @@ -111,7 +120,8 @@ fn root_cannot_cancel_deferred_slash_if_outside_deferring_period() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); start_era(4, 0, 4); @@ -131,7 +141,8 @@ fn root_cannot_cancel_out_of_bounds() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_noop!( ExternalValidatorSlashes::cancel_deferred_slash( @@ -152,7 +163,8 @@ fn root_cannot_cancel_duplicates() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_noop!( ExternalValidatorSlashes::cancel_deferred_slash(RuntimeOrigin::root(), 3, vec![0, 0]), @@ -169,13 +181,15 @@ fn root_cannot_cancel_if_not_sorted() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_ok!(ExternalValidatorSlashes::force_inject_slash( RuntimeOrigin::root(), 0, 2u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_noop!( ExternalValidatorSlashes::cancel_deferred_slash(RuntimeOrigin::root(), 3, vec![1, 0]), @@ -196,7 +210,8 @@ fn test_after_bonding_period_we_can_remove_slashes() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_eq!( @@ -206,7 +221,10 @@ fn test_after_bonding_period_we_can_remove_slashes() { percentage: Perbill::from_percent(75), confirmed: false, reporters: vec![], - slash_id: 0 + slash_id: 0, + offence_kind: OffenceKind::Custom(BoundedVec::truncate_from( + b"Test slash".to_vec() + )), }] ); @@ -226,6 +244,7 @@ fn test_on_offence_injects_offences() { new_test_ext().execute_with(|| { start_era(0, 0, 0); start_era(1, 1, 1); + PendingOffenceKind::::insert(0, 3u64, OffenceKind::LivenessOffence); Pallet::::on_offence( &[OffenceDetails { // 1 and 2 are invulnerables @@ -242,7 +261,8 @@ fn test_on_offence_injects_offences() { percentage: Perbill::from_percent(75), confirmed: false, reporters: vec![], - slash_id: 0 + slash_id: 0, + offence_kind: OffenceKind::LivenessOffence, }] ); }); @@ -253,7 +273,8 @@ fn test_on_offence_does_not_work_for_invulnerables() { new_test_ext().execute_with(|| { start_era(0, 0, 0); start_era(1, 1, 1); - // account 1 invulnerable + // account 1 invulnerable — populate kind so we test the invulnerable check, not missing kind + PendingOffenceKind::::insert(0, 1u64, OffenceKind::LivenessOffence); Pallet::::on_offence( &[OffenceDetails { offender: (1, ()), @@ -276,6 +297,7 @@ fn test_on_offence_does_not_work_if_slashing_disabled() { RuntimeOrigin::root(), SlashingModeOption::Disabled, )); + PendingOffenceKind::::insert(0, 3u64, OffenceKind::LivenessOffence); let weight = Pallet::::on_offence( &[OffenceDetails { // 1 and 2 are invulnerables @@ -303,7 +325,8 @@ fn defer_period_of_zero_confirms_immediately_slashes() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_eq!( Slashes::::get(get_slashing_era(0)), @@ -312,7 +335,10 @@ fn defer_period_of_zero_confirms_immediately_slashes() { percentage: Perbill::from_percent(75), confirmed: true, reporters: vec![], - slash_id: 0 + slash_id: 0, + offence_kind: OffenceKind::Custom(BoundedVec::truncate_from( + b"Test slash".to_vec() + )), }] ); }); @@ -327,7 +353,8 @@ fn we_cannot_cancel_anything_with_defer_period_zero() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_noop!( ExternalValidatorSlashes::cancel_deferred_slash(RuntimeOrigin::root(), 0, vec![0]), @@ -342,6 +369,7 @@ fn test_on_offence_defer_period_0() { crate::mock::DeferPeriodGetter::with_defer_period(0); start_era(0, 0, 0); start_era(1, 1, 1); + PendingOffenceKind::::insert(0, 3u64, OffenceKind::LivenessOffence); Pallet::::on_offence( &[OffenceDetails { // 1 and 2 are invulnerables @@ -359,7 +387,8 @@ fn test_on_offence_defer_period_0() { percentage: Perbill::from_percent(75), confirmed: true, reporters: vec![], - slash_id: 0 + slash_id: 0, + offence_kind: OffenceKind::LivenessOffence, }] ); start_era(2, 2, 2); @@ -373,6 +402,7 @@ fn test_slashes_command_matches_event() { crate::mock::DeferPeriodGetter::with_defer_period(0); start_era(0, 0, 0); start_era(1, 1, 1); + PendingOffenceKind::::insert(0, 3u64, OffenceKind::LivenessOffence); Pallet::::on_offence( &[OffenceDetails { // 1 and 2 are invulnerables @@ -391,7 +421,8 @@ fn test_slashes_command_matches_event() { percentage: Perbill::from_percent(75), confirmed: true, reporters: vec![], - slash_id: 0 + slash_id: 0, + offence_kind: OffenceKind::LivenessOffence, }] ); start_era(2, 2, 2); @@ -405,6 +436,122 @@ fn test_slashes_command_matches_event() { }); } +// ── WAD conversion tests ── +// MaxSlashWad in mock = 50_000_000_000_000_000 (5e16 = 5% in WAD format). +// Perbill(100%) = 1_000_000_000 inner. +// Formula: wad = perbill_inner * MaxSlashWad / 1e9 + +#[test] +fn wad_conversion_100_percent_slash_maps_to_max_slash_wad() { + new_test_ext().execute_with(|| { + crate::mock::DeferPeriodGetter::with_defer_period(0); + start_era(0, 0, 0); + start_era(1, 1, 1); + + PendingOffenceKind::::insert(0, 3u64, OffenceKind::LivenessOffence); + Pallet::::on_offence( + &[OffenceDetails { + offender: (3, ()), + reporters: vec![], + }], + &[Perbill::from_percent(100)], + 0, + ); + + start_era(2, 2, 2); + run_block(); + + let sent = MockOkOutboundQueue::last_sent_slashes(); + assert_eq!(sent.len(), 1); + // 100% → full MaxSlashWad = 5e16 + assert_eq!(sent[0].wad_to_slash, 50_000_000_000_000_000u128); + assert_eq!(sent[0].validator, 3); + }); +} + +#[test] +fn wad_conversion_50_percent_slash_maps_to_half_max_slash_wad() { + new_test_ext().execute_with(|| { + crate::mock::DeferPeriodGetter::with_defer_period(0); + start_era(0, 0, 0); + start_era(1, 1, 1); + + PendingOffenceKind::::insert(0, 3u64, OffenceKind::LivenessOffence); + Pallet::::on_offence( + &[OffenceDetails { + offender: (3, ()), + reporters: vec![], + }], + &[Perbill::from_percent(50)], + 0, + ); + + start_era(2, 2, 2); + run_block(); + + let sent = MockOkOutboundQueue::last_sent_slashes(); + assert_eq!(sent.len(), 1); + // 50% → MaxSlashWad / 2 = 2.5e16 + assert_eq!(sent[0].wad_to_slash, 25_000_000_000_000_000u128); + }); +} + +#[test] +fn wad_conversion_zero_percent_slash_maps_to_zero() { + new_test_ext().execute_with(|| { + crate::mock::DeferPeriodGetter::with_defer_period(0); + start_era(0, 0, 0); + start_era(1, 1, 1); + + PendingOffenceKind::::insert(0, 3u64, OffenceKind::LivenessOffence); + Pallet::::on_offence( + &[OffenceDetails { + offender: (3, ()), + reporters: vec![], + }], + &[Perbill::from_percent(0)], + 0, + ); + + start_era(2, 2, 2); + run_block(); + + // 0% slash → no slash recorded (compute_slash returns None for 0%) + let sent = MockOkOutboundQueue::last_sent_slashes(); + assert_eq!(sent.len(), 0); + }); +} + +#[test] +fn wad_conversion_carries_offence_kind_description() { + new_test_ext().execute_with(|| { + crate::mock::DeferPeriodGetter::with_defer_period(0); + start_era(0, 0, 0); + start_era(1, 1, 1); + + // Pre-populate a BabeEquivocation kind for session 0, validator 3. + PendingOffenceKind::::insert(0, 3u64, OffenceKind::BabeEquivocation); + + Pallet::::on_offence( + &[OffenceDetails { + offender: (3, ()), + reporters: vec![], + }], + &[Perbill::from_percent(75)], + 0, + ); + + start_era(2, 2, 2); + run_block(); + + let sent = MockOkOutboundQueue::last_sent_slashes(); + assert_eq!(sent.len(), 1); + // 75% → 75% of MaxSlashWad = 3.75e16 + assert_eq!(sent[0].wad_to_slash, 37_500_000_000_000_000u128); + assert_eq!(sent[0].description, "BABE equivocation"); + }); +} + #[test] fn test_on_offence_defer_period_0_messages_get_queued() { new_test_ext().execute_with(|| { @@ -413,6 +560,7 @@ fn test_on_offence_defer_period_0_messages_get_queued() { start_era(1, 1, 1); // The limit is 20, for i in 0..25 { + PendingOffenceKind::::insert(0, 3 + i, OffenceKind::LivenessOffence); Pallet::::on_offence( &[OffenceDetails { // 1 and 2 are invulnerables @@ -450,6 +598,7 @@ fn test_account_id_encoding() { slash_id: 1, percentage: Perbill::default(), confirmed: true, + offence_kind: OffenceKind::LivenessOffence, }; let encoded_account = slash.validator.encode(); @@ -466,6 +615,7 @@ fn test_on_offence_defer_period_0_messages_get_queued_across_eras() { start_era(1, 1, 1); // The limit is 20, for i in 0..25 { + PendingOffenceKind::::insert(0, 3 + i, OffenceKind::LivenessOffence); Pallet::::on_offence( &[OffenceDetails { // 1 and 2 are invulnerables @@ -487,6 +637,7 @@ fn test_on_offence_defer_period_0_messages_get_queued_across_eras() { // We have 5 non-dispatched, which should accumulate // We shoulld have 30 after we initialie era 3 for i in 0..25 { + PendingOffenceKind::::insert(2, 3 + i, OffenceKind::LivenessOffence); Pallet::::on_offence( &[OffenceDetails { // 1 and 2 are invulnerables @@ -512,6 +663,213 @@ fn test_on_offence_defer_period_0_messages_get_queued_across_eras() { }); } +// ── PendingOffenceKind & EquivocationReportWrapper tests ── + +#[test] +fn on_offence_reads_pending_offence_kind_from_double_map() { + new_test_ext().execute_with(|| { + start_era(0, 0, 0); + start_era(1, 1, 1); + + // Pre-populate PendingOffenceKind for validator 3 at session 0. + PendingOffenceKind::::insert(0, 3u64, OffenceKind::BabeEquivocation); + + Pallet::::on_offence( + &[OffenceDetails { + offender: (3, ()), + reporters: vec![], + }], + &[Perbill::from_percent(75)], + 0, + ); + + assert_eq!( + Slashes::::get(get_slashing_era(0)), + vec![Slash { + validator: 3, + percentage: Perbill::from_percent(75), + confirmed: false, + reporters: vec![], + slash_id: 0, + offence_kind: OffenceKind::BabeEquivocation, + }] + ); + + // Entry should have been consumed. + assert_eq!(PendingOffenceKind::::get(0, 3u64), None); + }); +} + +#[test] +fn pending_offence_kind_is_session_isolated() { + new_test_ext().execute_with(|| { + start_era(0, 0, 0); + start_era(1, 1, 1); + + // Same validator, different kinds in different sessions. + PendingOffenceKind::::insert(0, 3u64, OffenceKind::BabeEquivocation); + PendingOffenceKind::::insert(1, 3u64, OffenceKind::GrandpaEquivocation); + + // Report at session 0 — should use BabeEquivocation. + Pallet::::on_offence( + &[OffenceDetails { + offender: (3, ()), + reporters: vec![], + }], + &[Perbill::from_percent(50)], + 0, + ); + + // Session 0 consumed, session 1 untouched. + assert_eq!(PendingOffenceKind::::get(0, 3u64), None); + assert_eq!( + PendingOffenceKind::::get(1, 3u64), + Some(OffenceKind::GrandpaEquivocation), + ); + }); +} + +#[test] +fn wrapper_filters_historical_offence_before_bonding_period() { + new_test_ext().execute_with(|| { + start_era(0, 0, 0); + start_era(1, 1, 1); + MockInnerReporter::reset(); + + // BondedEras now contains [(0,0,0), (1,1,1)]. + // An offence at session 0 is within the bonding period — should pass. + let result = MockBabeWrapper::report_offence( + Vec::::new(), + MockOffence { + session_index: 0, + offenders: vec![(3, ())], + }, + ); + assert!(result.is_ok()); + assert!(MockInnerReporter::was_called()); + + // The mock reporter doesn't trigger on_offence, so manually consume the entry. + assert_eq!( + PendingOffenceKind::::take(0, 3u64), + Some(OffenceKind::BabeEquivocation), + ); + + // Advance eras until era 0 drops out of BondedEras. + // BondingDuration = 5, so after era 6 starts, era 0 is pruned. + for i in 2..=7 { + start_era(i, i, i as u64); + } + + MockInnerReporter::reset(); + + // Session 0 now predates the bonding period — should be silently discarded. + let result = MockBabeWrapper::report_offence( + Vec::::new(), + MockOffence { + session_index: 0, + offenders: vec![(3, ())], + }, + ); + assert!(result.is_ok()); + assert!(!MockInnerReporter::was_called()); + + // No PendingOffenceKind should have been written. + assert_eq!(PendingOffenceKind::::get(0, 3u64), None); + }); +} + +#[test] +fn wrapper_sets_pending_offence_kind_per_session_and_offender() { + new_test_ext().execute_with(|| { + start_era(0, 0, 0); + start_era(1, 1, 1); + MockInnerReporter::reset(); + + let _ = MockBabeWrapper::report_offence( + Vec::::new(), + MockOffence { + session_index: 0, + offenders: vec![(3, ()), (4, ())], + }, + ); + + // Both offenders should have entries at session 0. + assert_eq!( + PendingOffenceKind::::get(0, 3u64), + Some(OffenceKind::BabeEquivocation), + ); + assert_eq!( + PendingOffenceKind::::get(0, 4u64), + Some(OffenceKind::BabeEquivocation), + ); + // No entry at a different session. + assert_eq!(PendingOffenceKind::::get(1, 3u64), None); + }); +} + +#[test] +fn wrapper_cleans_up_pending_offence_kind_on_error() { + new_test_ext().execute_with(|| { + start_era(0, 0, 0); + start_era(1, 1, 1); + MockInnerReporter::reset(); + MockInnerReporter::set_should_fail(true); + + let result = MockBabeWrapper::report_offence( + Vec::::new(), + MockOffence { + session_index: 0, + offenders: vec![(3, ()), (4, ())], + }, + ); + + assert!(result.is_err()); + // Entries should have been cleaned up. + assert_eq!(PendingOffenceKind::::get(0, 3u64), None); + assert_eq!(PendingOffenceKind::::get(0, 4u64), None); + }); +} + +#[test] +fn wrapper_error_cleanup_does_not_affect_other_sessions() { + new_test_ext().execute_with(|| { + start_era(0, 0, 0); + start_era(1, 1, 1); + MockInnerReporter::reset(); + + // Successfully report at session 0. + let _ = MockGrandpaWrapper::report_offence( + Vec::::new(), + MockOffence { + session_index: 0, + offenders: vec![(3, ())], + }, + ); + assert_eq!( + PendingOffenceKind::::get(0, 3u64), + Some(OffenceKind::GrandpaEquivocation), + ); + + // Now fail a report at session 1 for the same validator. + MockInnerReporter::set_should_fail(true); + let result = MockBabeWrapper::report_offence( + Vec::::new(), + MockOffence { + session_index: 1, + offenders: vec![(3, ())], + }, + ); + assert!(result.is_err()); + + // Session 1 cleaned up, session 0 untouched. + assert_eq!(PendingOffenceKind::::get(1, 3u64), None); + assert_eq!( + PendingOffenceKind::::get(0, 3u64), + Some(OffenceKind::GrandpaEquivocation), + ); + }); +} + fn start_era(era_index: EraIndex, session_index: SessionIndex, external_idx: u64) { Pallet::::on_era_start(era_index, session_index, external_idx); crate::mock::MockEraIndexProvider::with_era(era_index); diff --git a/operator/runtime/common/src/slashes_adapter.rs b/operator/runtime/common/src/slashes_adapter.rs index e28aabc4..74fa5c58 100644 --- a/operator/runtime/common/src/slashes_adapter.rs +++ b/operator/runtime/common/src/slashes_adapter.rs @@ -111,8 +111,8 @@ fn encode_slashing_request( let slashing_request = SlashingRequest { operator: Address::from(slash_operator.validator.0), strategies: strategies.clone(), - wadsToSlash: wads_to_slash, // We only have one strategy deployed - description: "Slashing validator".into(), + wadsToSlash: wads_to_slash, + description: slash_operator.description.clone().into(), }; slashings.push(slashing_request); diff --git a/operator/runtime/mainnet/src/configs/mod.rs b/operator/runtime/mainnet/src/configs/mod.rs index 2d97fc46..1499a9c5 100644 --- a/operator/runtime/mainnet/src/configs/mod.rs +++ b/operator/runtime/mainnet/src/configs/mod.rs @@ -321,8 +321,16 @@ impl pallet_babe::Config for Runtime { type KeyOwnerProof = >::Proof; - type EquivocationReportSystem = - pallet_babe::EquivocationReportSystem; + type EquivocationReportSystem = pallet_babe::EquivocationReportSystem< + Self, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::BabeEquivocation, + >, + Historical, + ReportLongevity, + >; } impl pallet_timestamp::Config for Runtime { @@ -401,7 +409,11 @@ impl pallet_im_online::Config for Runtime { type RuntimeEvent = RuntimeEvent; type ValidatorSet = Historical; type NextSessionRotation = Babe; - type ReportUnresponsiveness = Offences; + type ReportUnresponsiveness = pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::ImOnlineUnresponsive, + >; type UnsignedPriority = ImOnlineUnsignedPriority; type WeightInfo = crate::weights::pallet_im_online::WeightInfo; } @@ -424,7 +436,11 @@ impl pallet_grandpa::Config for Runtime { type KeyOwnerProof = >::Proof; type EquivocationReportSystem = pallet_grandpa::EquivocationReportSystem< Self, - Offences, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::GrandpaEquivocation, + >, Historical, EquivocationReportPeriodInBlocks, >; @@ -501,8 +517,16 @@ impl pallet_beefy::Config for Runtime { type AncestryHelper = BeefyMmrLeaf; type WeightInfo = (); type KeyOwnerProof = >::Proof; - type EquivocationReportSystem = - pallet_beefy::EquivocationReportSystem; + type EquivocationReportSystem = pallet_beefy::EquivocationReportSystem< + Self, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::BeefyEquivocation, + >, + Historical, + ReportLongevity, + >; } parameter_types! { @@ -1701,6 +1725,7 @@ impl pallet_external_validator_slashes::Config for Runtime { type EraIndexProvider = ExternalValidators; type InvulnerablesProvider = ExternalValidators; type ExternalIndexProvider = ExternalValidators; + type MaxSlashWad = runtime_params::dynamic_params::runtime_config::MaxSlashWad; type QueuedSlashesProcessedPerBlock = ConstU32<10>; type WeightInfo = mainnet_weights::pallet_external_validator_slashes::WeightInfo; type SendMessage = SlashesSendAdapter; diff --git a/operator/runtime/mainnet/src/configs/runtime_params.rs b/operator/runtime/mainnet/src/configs/runtime_params.rs index 605c9f73..aa35f269 100644 --- a/operator/runtime/mainnet/src/configs/runtime_params.rs +++ b/operator/runtime/mainnet/src/configs/runtime_params.rs @@ -417,6 +417,16 @@ pub mod dynamic_params { BoundedVec::truncate_from(vec![]); // ╚══════════════════════ EigenLayer Rewards V2 ═══════════════════════╝ + + // ╔══════════════════════ EigenLayer Slashing ═══════════════════════╗ + + #[codec(index = 46)] + #[allow(non_upper_case_globals)] + /// Maximum WAD value for EigenLayer slashing. Maps Perbill(100%) to this value. + /// 5e16 = 5% in WAD format (1e18 = 100%). + pub static MaxSlashWad: u128 = 50_000_000_000_000_000u128; + + // ╚══════════════════════ EigenLayer Slashing ═══════════════════════╝ } } diff --git a/operator/runtime/stagenet/src/configs/mod.rs b/operator/runtime/stagenet/src/configs/mod.rs index fb4cc4bb..299554c6 100644 --- a/operator/runtime/stagenet/src/configs/mod.rs +++ b/operator/runtime/stagenet/src/configs/mod.rs @@ -321,8 +321,16 @@ impl pallet_babe::Config for Runtime { type KeyOwnerProof = >::Proof; - type EquivocationReportSystem = - pallet_babe::EquivocationReportSystem; + type EquivocationReportSystem = pallet_babe::EquivocationReportSystem< + Self, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::BabeEquivocation, + >, + Historical, + ReportLongevity, + >; } impl pallet_timestamp::Config for Runtime { @@ -400,7 +408,11 @@ impl pallet_im_online::Config for Runtime { type RuntimeEvent = RuntimeEvent; type ValidatorSet = Historical; type NextSessionRotation = Babe; - type ReportUnresponsiveness = Offences; + type ReportUnresponsiveness = pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::ImOnlineUnresponsive, + >; type UnsignedPriority = ImOnlineUnsignedPriority; type WeightInfo = crate::weights::pallet_im_online::WeightInfo; } @@ -423,7 +435,11 @@ impl pallet_grandpa::Config for Runtime { type KeyOwnerProof = >::Proof; type EquivocationReportSystem = pallet_grandpa::EquivocationReportSystem< Self, - Offences, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::GrandpaEquivocation, + >, Historical, EquivocationReportPeriodInBlocks, >; @@ -498,8 +514,16 @@ impl pallet_beefy::Config for Runtime { type AncestryHelper = BeefyMmrLeaf; type WeightInfo = (); type KeyOwnerProof = >::Proof; - type EquivocationReportSystem = - pallet_beefy::EquivocationReportSystem; + type EquivocationReportSystem = pallet_beefy::EquivocationReportSystem< + Self, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::BeefyEquivocation, + >, + Historical, + ReportLongevity, + >; } parameter_types! { @@ -1697,6 +1721,7 @@ impl pallet_external_validator_slashes::Config for Runtime { type EraIndexProvider = ExternalValidators; type InvulnerablesProvider = ExternalValidators; type ExternalIndexProvider = ExternalValidators; + type MaxSlashWad = runtime_params::dynamic_params::runtime_config::MaxSlashWad; type QueuedSlashesProcessedPerBlock = ConstU32<10>; type WeightInfo = stagenet_weights::pallet_external_validator_slashes::WeightInfo; type SendMessage = SlashesSendAdapter; diff --git a/operator/runtime/stagenet/src/configs/runtime_params.rs b/operator/runtime/stagenet/src/configs/runtime_params.rs index f05caff5..d7775020 100644 --- a/operator/runtime/stagenet/src/configs/runtime_params.rs +++ b/operator/runtime/stagenet/src/configs/runtime_params.rs @@ -424,6 +424,16 @@ pub mod dynamic_params { BoundedVec::truncate_from(vec![]); // ╚══════════════════════ EigenLayer Rewards V2 ═══════════════════════╝ + + // ╔══════════════════════ EigenLayer Slashing ═══════════════════════╗ + + #[codec(index = 46)] + #[allow(non_upper_case_globals)] + /// Maximum WAD value for EigenLayer slashing. Maps Perbill(100%) to this value. + /// 5e16 = 5% in WAD format (1e18 = 100%). + pub static MaxSlashWad: u128 = 50_000_000_000_000_000u128; + + // ╚══════════════════════ EigenLayer Slashing ═══════════════════════╝ } } diff --git a/operator/runtime/testnet/src/configs/mod.rs b/operator/runtime/testnet/src/configs/mod.rs index 5b9305a9..ccf9d48a 100644 --- a/operator/runtime/testnet/src/configs/mod.rs +++ b/operator/runtime/testnet/src/configs/mod.rs @@ -321,8 +321,16 @@ impl pallet_babe::Config for Runtime { type KeyOwnerProof = >::Proof; - type EquivocationReportSystem = - pallet_babe::EquivocationReportSystem; + type EquivocationReportSystem = pallet_babe::EquivocationReportSystem< + Self, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::BabeEquivocation, + >, + Historical, + ReportLongevity, + >; } impl pallet_timestamp::Config for Runtime { @@ -400,7 +408,11 @@ impl pallet_im_online::Config for Runtime { type RuntimeEvent = RuntimeEvent; type ValidatorSet = Historical; type NextSessionRotation = Babe; - type ReportUnresponsiveness = Offences; + type ReportUnresponsiveness = pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::ImOnlineUnresponsive, + >; type UnsignedPriority = ImOnlineUnsignedPriority; type WeightInfo = crate::weights::pallet_im_online::WeightInfo; } @@ -423,7 +435,11 @@ impl pallet_grandpa::Config for Runtime { type KeyOwnerProof = >::Proof; type EquivocationReportSystem = pallet_grandpa::EquivocationReportSystem< Self, - Offences, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::GrandpaEquivocation, + >, Historical, EquivocationReportPeriodInBlocks, >; @@ -501,8 +517,16 @@ impl pallet_beefy::Config for Runtime { type AncestryHelper = BeefyMmrLeaf; type WeightInfo = (); type KeyOwnerProof = >::Proof; - type EquivocationReportSystem = - pallet_beefy::EquivocationReportSystem; + type EquivocationReportSystem = pallet_beefy::EquivocationReportSystem< + Self, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::BeefyEquivocation, + >, + Historical, + ReportLongevity, + >; } parameter_types! { @@ -1700,6 +1724,7 @@ impl pallet_external_validator_slashes::Config for Runtime { type EraIndexProvider = ExternalValidators; type InvulnerablesProvider = ExternalValidators; type ExternalIndexProvider = ExternalValidators; + type MaxSlashWad = runtime_params::dynamic_params::runtime_config::MaxSlashWad; type QueuedSlashesProcessedPerBlock = ConstU32<10>; type WeightInfo = testnet_weights::pallet_external_validator_slashes::WeightInfo; type SendMessage = SlashesSendAdapter; diff --git a/operator/runtime/testnet/src/configs/runtime_params.rs b/operator/runtime/testnet/src/configs/runtime_params.rs index bc218f6c..5753d215 100644 --- a/operator/runtime/testnet/src/configs/runtime_params.rs +++ b/operator/runtime/testnet/src/configs/runtime_params.rs @@ -419,6 +419,16 @@ pub mod dynamic_params { BoundedVec::truncate_from(vec![]); // ╚══════════════════════ EigenLayer Rewards V2 ═══════════════════════╝ + + // ╔══════════════════════ EigenLayer Slashing ═══════════════════════╗ + + #[codec(index = 46)] + #[allow(non_upper_case_globals)] + /// Maximum WAD value for EigenLayer slashing. Maps Perbill(100%) to this value. + /// 5e16 = 5% in WAD format (1e18 = 100%). + pub static MaxSlashWad: u128 = 50_000_000_000_000_000u128; + + // ╚══════════════════════ EigenLayer Slashing ═══════════════════════╝ } } diff --git a/test/.papi/descriptors/package.json b/test/.papi/descriptors/package.json index d7be6139..d5f24906 100644 --- a/test/.papi/descriptors/package.json +++ b/test/.papi/descriptors/package.json @@ -1,5 +1,5 @@ { - "version": "0.1.0-autogenerated.13357056092938763018", + "version": "0.1.0-autogenerated.18296316742446681711", "name": "@polkadot-api/descriptors", "files": [ "dist" diff --git a/test/.papi/metadata/datahaven.scale b/test/.papi/metadata/datahaven.scale index 359a4996ce83cd35c179a02ef7773a7cd14400ef..d0ecbe092134c6f09ec29777cb7151c9c7c39f20 100644 GIT binary patch delta 29027 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 Q;62(2{6)1TjRTbb2VX8UKL7v# delta 28159 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μOldv564T77*#yN6J>HF=UezRBb5H+ei5 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 diff --git a/test/e2e/suites/slash.test.ts b/test/e2e/suites/slash.test.ts index 87e370f6..c212cd90 100644 --- a/test/e2e/suites/slash.test.ts +++ b/test/e2e/suites/slash.test.ts @@ -1,9 +1,11 @@ import { beforeAll, describe, expect, it } from "bun:test"; -import { FixedSizeBinary } from "polkadot-api"; +import { $ } from "bun"; +import { Binary, FixedSizeBinary } from "polkadot-api"; import { CROSS_CHAIN_TIMEOUTS, getPapiSigner, logger } from "utils"; import type { Address } from "viem"; import { getContractInstance, parseDeploymentsFile } from "../../utils/contracts"; import { waitForDataHavenEvent } from "../../utils/events"; +import { waitFor } from "../../utils/waits"; import { BaseTestSuite } from "../framework"; class SlashTestSuite extends BaseTestSuite { @@ -15,6 +17,10 @@ class SlashTestSuite extends BaseTestSuite { // Set up hooks in constructor this.setupHooks(); } + + getNetworkId(): string { + return this.networkId; + } } // Create the test suite instance @@ -129,7 +135,11 @@ describe("Should slash an operator", () => { const sudoSlashCall = dhApi.tx.ExternalValidatorsSlashes.force_inject_slash({ validator, era: activeEra?.index || 0, // Will fail if active era is 0. !! Important !! Sometimes for the inject to work (because of some latency) we need to inject in the `activeEra.index + 1` - percentage: 20 + percentage: 20, + offence_kind: { + type: "Custom", + value: Binary.fromText("Manual slash: E2E test") + } }); const sudoTx = dhApi.tx.Sudo.sudo({ call: sudoSlashCall.decodedCall @@ -159,4 +169,106 @@ describe("Should slash an operator", () => { } logger.info("Slashes message sent"); }, 560000); + + it("should detect and slash an unresponsive validator (liveness)", async () => { + const networkId = suite.getNetworkId(); + const bobContainer = `datahaven-bob-${networkId}`; + + // Drain any prior SlashReported events so we only see events from this test. + await dhApi.event.ExternalValidatorsSlashes.SlashReported.pull(); + + const activeEra = await dhApi.query.ExternalValidators.ActiveEra.getValue({ at: "best" }); + const eraAtStart = activeEra?.index ?? 0; + const sessionAtStart = await dhApi.query.Session.CurrentIndex.getValue({ at: "best" }); + logger.info(`Liveness test start — era: ${eraAtStart}, session: ${sessionAtStart}`); + + // Pause bob to simulate a liveness failure (missed heartbeats). + // Using pause/unpause instead of stop/start preserves bob's process + // state (GRANDPA voter, peer connections, keystore) so finality can + // resume quickly once unpaused. + logger.info(`Pausing bob container: ${bobContainer}`); + await $`docker pause ${bobContainer}`.quiet(); + logger.info("Bob container paused. Waiting for sessions to elapse..."); + + let slashReportedEvent: any; + try { + // Wait for at least TWO full sessions so pallet_im_online detects bob's + // missing heartbeats at a session boundary. + // + // Why two sessions: + // 1. Bob may have already sent his heartbeat for the current session + // before being paused, so the first session boundary won't report him. + // 2. The NEXT full session (where bob is offline from start to finish) + // will trigger the unresponsiveness report at its boundary. + // + // Timing with BABE c=(1,4) and PrimaryAndSecondaryVRFSlots: + // - Alice alone produces ~62.5% of blocks → ~9.6s per block on average. + // - 10 blocks/session → ~96s per session with alice only. + // - 2 sessions = ~192s. We use 200s for margin. + await Bun.sleep(200_000); + + // Unpause bob to restore GRANDPA finality (needs 2/2 validators). + // Bob's process resumes immediately with full state, so he can vote + // on the pending blocks that alice produced while he was paused. + logger.info("Unpausing bob container..."); + await $`docker unpause ${bobContainer}`.quiet(); + logger.info("Bob unpaused. Waiting for finality and SlashReported event..."); + + // Poll for a SlashReported event from the ExternalValidatorsSlashes pallet. + // + // Why SlashReported instead of Slashes storage: + // pallet_im_online's UnresponsivenessOffence::slash_fraction() formula + // gives 0% for small validator sets (1 out of 2 offline is below the + // 10%+1 threshold). With 0% fraction, compute_slash returns None and + // no Slashes entry is created. However, on_offence still emits the + // SlashReported event, which proves the full detection pipeline works: + // pallet_im_online → EquivocationReportWrapper → pallet_offences → on_offence. + // + // After unpausing bob, GRANDPA finality catches up and the events from + // alice's blocks (produced during the pause) become finalized and visible + // to the PAPI event subscription. + let pollCount = 0; + const collectedEvents: any[] = []; + await waitFor({ + lambda: async () => { + pollCount++; + const events = await dhApi.event.ExternalValidatorsSlashes.SlashReported.pull(); + for (const event of events) { + const payload = (event as any)?.payload ?? event; + collectedEvents.push(payload); + logger.info( + `[poll ${pollCount}] SlashReported event: validator=${payload?.validator}, ` + + `fraction=${JSON.stringify(payload?.fraction)}, slash_era=${payload?.slash_era}` + ); + } + if (collectedEvents.length > 0) { + slashReportedEvent = collectedEvents[0]; + return true; + } + if (pollCount % 10 === 0) { + const curEra = await dhApi.query.ExternalValidators.ActiveEra.getValue({ at: "best" }); + const curSession = await dhApi.query.Session.CurrentIndex.getValue({ at: "best" }); + logger.info( + `[poll ${pollCount}] era: ${curEra?.index}, session: ${curSession} (started at era=${eraAtStart}, session=${sessionAtStart})` + ); + } + return false; + }, + iterations: 60, + delay: 5000, + errorMessage: "SlashReported event not found after pausing bob for liveness detection" + }); + } finally { + // Ensure bob is always unpaused so the network stays healthy for teardown + await $`docker unpause ${bobContainer}`.nothrow().quiet(); + } + + expect(slashReportedEvent).toBeDefined(); + logger.info( + "Liveness offence confirmed via SlashReported: " + + `validator=${slashReportedEvent.validator}, ` + + `fraction=${JSON.stringify(slashReportedEvent.fraction)}, ` + + `slash_era=${slashReportedEvent.slash_era}` + ); + }, 600_000); });