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 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 359a4996..d0ecbe09 100644 Binary files a/test/.papi/metadata/datahaven.scale and b/test/.papi/metadata/datahaven.scale differ 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); });