Merge branch 'main' into feat/add-validator-submitter-ci-job

This commit is contained in:
Ahmad Kaouk 2026-03-05 15:21:41 +01:00 committed by GitHub
commit 9931759467
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1044 additions and 176 deletions

148
operator/Cargo.lock generated
View file

@ -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",

View file

@ -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

View file

@ -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::<T::AccountId, T::SlashId>::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::<T>::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::<T>::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::<T::AccountId, T::SlashId>::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::<T>::set(queue);

View file

@ -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<u8, ConstU32<256>>),
}
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<AccountId> {
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<u128>;
/// How many queued slashes are being processed per block.
#[pallet::constant]
type QueuedSlashesProcessedPerBlock: Get<u32>;
@ -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<T: Config> = 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<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
SessionIndex,
Twox64Concat,
T::ValidatorId,
OffenceKind,
OptionQuery,
>;
#[pallet::genesis_config]
#[derive(frame_support::DefaultNoBound)]
pub struct GenesisConfig<T: Config> {
@ -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::<T>::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<T: Config>
OnOffenceHandler<T::AccountId, pallet_session::historical::IdentificationTuple<T>, Weight>
for Pallet<T>
@ -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::<T>::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<T: Config> Pallet<T> {
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<AccountId, SlashId> {
pub percentage: Perbill,
// Whether the slash is confirmed or still needs to go through deferred period
pub confirmed: bool,
}
impl<AccountId, SlashId: One> Slash<AccountId, SlashId> {
/// 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<T: Config>(
slash_era: EraIndex,
stash: T::AccountId,
slash_defer_duration: EraIndex,
offence_kind: OffenceKind,
) -> Option<Slash<T::AccountId, T::SlashId>> {
let prior_slash_p = ValidatorSlashInEra::<T>::get(slash_era, &stash).unwrap_or(Zero::zero());
@ -707,6 +797,7 @@ pub(crate) fn compute_slash<T: Config>(
slash_id,
reporters: Vec::new(),
confirmed,
offence_kind,
})
}
@ -714,3 +805,107 @@ pub(crate) fn compute_slash<T: Config>(
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<ValidatorId> {
fn validator_id(&self) -> &ValidatorId;
}
impl<V, F> HasValidatorId<V> 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<T, Inner, Kind>(PhantomData<(T, Inner, Kind)>);
impl<T, Inner, Kind, R, O, Id> ReportOffence<R, Id, O> for EquivocationReportWrapper<T, Inner, Kind>
where
T: Config,
Inner: ReportOffence<R, Id, O>,
O: Offence<Id>,
Kind: OffenceKindProvider,
Id: HasValidatorId<T::ValidatorId>,
{
fn report_offence(reporters: Vec<R>, offence: O) -> Result<(), OffenceError> {
// Discard offences from before the bonding period.
let offence_session = offence.session_index();
let bonded_eras = pallet::BondedEras::<T>::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::<T>::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::<T>::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
}
}

View file

@ -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<EraIndex> = const { RefCell::new(0) };
pub static DEFER_PERIOD: RefCell<EraIndex> = const { RefCell::new(2) };
pub static SENT_ETHEREUM_MESSAGE_NONCE: RefCell<u64> = const { RefCell::new(0) };
pub static MOCK_REPORT_OFFENCE_SHOULD_FAIL: RefCell<bool> = const { RefCell::new(false) };
pub static MOCK_REPORT_OFFENCE_CALLED: RefCell<bool> = const { RefCell::new(false) };
pub static LAST_SENT_SLASHES: RefCell<Vec<crate::SlashData<AccountId>>> = RefCell::new(Vec::new());
}
impl MockEraIndexProvider {
@ -215,10 +217,16 @@ impl DeferPeriodGetter {
}
pub struct MockOkOutboundQueue;
impl MockOkOutboundQueue {
pub fn last_sent_slashes() -> Vec<crate::SlashData<AccountId>> {
LAST_SENT_SLASHES.with(|r| r.borrow().clone())
}
}
impl crate::SendMessage<AccountId> for MockOkOutboundQueue {
type Ticket = ();
type Message = ();
fn build(_: &Vec<crate::SlashData<AccountId>>, _: u32) -> Option<Self::Ticket> {
fn build(slashes: &Vec<crate::SlashData<AccountId>>, _: u32) -> Option<Self::Ticket> {
LAST_SENT_SLASHES.with(|r| *r.borrow_mut() = slashes.clone());
Some(())
}
fn validate(_: Self::Ticket) -> Result<Self::Ticket, SendError> {
@ -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<u64, Option<u64>> 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<R, Id, O: Offence<Id>> ReportOffence<R, Id, O> for MockInnerReporter {
fn report_offence(_reporters: Vec<R>, _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<Test, MockInnerReporter, crate::BabeEquivocation>;
/// Type alias for the wrapper using the mock reporter with GrandpaEquivocation kind.
pub type MockGrandpaWrapper =
crate::EquivocationReportWrapper<Test, MockInnerReporter, crate::GrandpaEquivocation>;
pub fn run_block() {
run_to_block(System::block_number() + 1);
}

View file

@ -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::<Test>::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::<Test>::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::<Test>::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::<Test>::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::<Test>::insert(0, 3u64, OffenceKind::LivenessOffence);
Pallet::<Test>::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::<Test>::insert(0, 1u64, OffenceKind::LivenessOffence);
Pallet::<Test>::on_offence(
&[OffenceDetails {
offender: (1, ()),
@ -276,6 +297,7 @@ fn test_on_offence_does_not_work_if_slashing_disabled() {
RuntimeOrigin::root(),
SlashingModeOption::Disabled,
));
PendingOffenceKind::<Test>::insert(0, 3u64, OffenceKind::LivenessOffence);
let weight = Pallet::<Test>::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::<Test>::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::<Test>::insert(0, 3u64, OffenceKind::LivenessOffence);
Pallet::<Test>::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::<Test>::insert(0, 3u64, OffenceKind::LivenessOffence);
Pallet::<Test>::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::<Test>::insert(0, 3u64, OffenceKind::LivenessOffence);
Pallet::<Test>::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::<Test>::insert(0, 3u64, OffenceKind::LivenessOffence);
Pallet::<Test>::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::<Test>::insert(0, 3u64, OffenceKind::LivenessOffence);
Pallet::<Test>::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::<Test>::insert(0, 3u64, OffenceKind::BabeEquivocation);
Pallet::<Test>::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::<Test>::insert(0, 3 + i, OffenceKind::LivenessOffence);
Pallet::<Test>::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::<Test>::insert(0, 3 + i, OffenceKind::LivenessOffence);
Pallet::<Test>::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::<Test>::insert(2, 3 + i, OffenceKind::LivenessOffence);
Pallet::<Test>::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::<Test>::insert(0, 3u64, OffenceKind::BabeEquivocation);
Pallet::<Test>::on_offence(
&[OffenceDetails {
offender: (3, ()),
reporters: vec![],
}],
&[Perbill::from_percent(75)],
0,
);
assert_eq!(
Slashes::<Test>::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::<Test>::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::<Test>::insert(0, 3u64, OffenceKind::BabeEquivocation);
PendingOffenceKind::<Test>::insert(1, 3u64, OffenceKind::GrandpaEquivocation);
// Report at session 0 — should use BabeEquivocation.
Pallet::<Test>::on_offence(
&[OffenceDetails {
offender: (3, ()),
reporters: vec![],
}],
&[Perbill::from_percent(50)],
0,
);
// Session 0 consumed, session 1 untouched.
assert_eq!(PendingOffenceKind::<Test>::get(0, 3u64), None);
assert_eq!(
PendingOffenceKind::<Test>::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::<u64>::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::<Test>::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::<u64>::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::<Test>::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::<u64>::new(),
MockOffence {
session_index: 0,
offenders: vec![(3, ()), (4, ())],
},
);
// Both offenders should have entries at session 0.
assert_eq!(
PendingOffenceKind::<Test>::get(0, 3u64),
Some(OffenceKind::BabeEquivocation),
);
assert_eq!(
PendingOffenceKind::<Test>::get(0, 4u64),
Some(OffenceKind::BabeEquivocation),
);
// No entry at a different session.
assert_eq!(PendingOffenceKind::<Test>::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::<u64>::new(),
MockOffence {
session_index: 0,
offenders: vec![(3, ()), (4, ())],
},
);
assert!(result.is_err());
// Entries should have been cleaned up.
assert_eq!(PendingOffenceKind::<Test>::get(0, 3u64), None);
assert_eq!(PendingOffenceKind::<Test>::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::<u64>::new(),
MockOffence {
session_index: 0,
offenders: vec![(3, ())],
},
);
assert_eq!(
PendingOffenceKind::<Test>::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::<u64>::new(),
MockOffence {
session_index: 1,
offenders: vec![(3, ())],
},
);
assert!(result.is_err());
// Session 1 cleaned up, session 0 untouched.
assert_eq!(PendingOffenceKind::<Test>::get(1, 3u64), None);
assert_eq!(
PendingOffenceKind::<Test>::get(0, 3u64),
Some(OffenceKind::GrandpaEquivocation),
);
});
}
fn start_era(era_index: EraIndex, session_index: SessionIndex, external_idx: u64) {
Pallet::<Test>::on_era_start(era_index, session_index, external_idx);
crate::mock::MockEraIndexProvider::with_era(era_index);

View file

@ -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);

View file

@ -321,8 +321,16 @@ impl pallet_babe::Config for Runtime {
type KeyOwnerProof =
<Historical as KeyOwnerProofSystem<(KeyTypeId, pallet_babe::AuthorityId)>>::Proof;
type EquivocationReportSystem =
pallet_babe::EquivocationReportSystem<Self, Offences, Historical, ReportLongevity>;
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<Runtime>;
}
@ -424,7 +436,11 @@ impl pallet_grandpa::Config for Runtime {
type KeyOwnerProof = <Historical as KeyOwnerProofSystem<(KeyTypeId, GrandpaId)>>::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 = <Historical as KeyOwnerProofSystem<(KeyTypeId, BeefyId)>>::Proof;
type EquivocationReportSystem =
pallet_beefy::EquivocationReportSystem<Self, Offences, Historical, ReportLongevity>;
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<Runtime>;
type SendMessage = SlashesSendAdapter;

View file

@ -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 ═══════════════════════╝
}
}

View file

@ -321,8 +321,16 @@ impl pallet_babe::Config for Runtime {
type KeyOwnerProof =
<Historical as KeyOwnerProofSystem<(KeyTypeId, pallet_babe::AuthorityId)>>::Proof;
type EquivocationReportSystem =
pallet_babe::EquivocationReportSystem<Self, Offences, Historical, ReportLongevity>;
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<Runtime>;
}
@ -423,7 +435,11 @@ impl pallet_grandpa::Config for Runtime {
type KeyOwnerProof = <Historical as KeyOwnerProofSystem<(KeyTypeId, GrandpaId)>>::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 = <Historical as KeyOwnerProofSystem<(KeyTypeId, BeefyId)>>::Proof;
type EquivocationReportSystem =
pallet_beefy::EquivocationReportSystem<Self, Offences, Historical, ReportLongevity>;
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<Runtime>;
type SendMessage = SlashesSendAdapter;

View file

@ -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 ═══════════════════════╝
}
}

View file

@ -321,8 +321,16 @@ impl pallet_babe::Config for Runtime {
type KeyOwnerProof =
<Historical as KeyOwnerProofSystem<(KeyTypeId, pallet_babe::AuthorityId)>>::Proof;
type EquivocationReportSystem =
pallet_babe::EquivocationReportSystem<Self, Offences, Historical, ReportLongevity>;
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<Runtime>;
}
@ -423,7 +435,11 @@ impl pallet_grandpa::Config for Runtime {
type KeyOwnerProof = <Historical as KeyOwnerProofSystem<(KeyTypeId, GrandpaId)>>::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 = <Historical as KeyOwnerProofSystem<(KeyTypeId, BeefyId)>>::Proof;
type EquivocationReportSystem =
pallet_beefy::EquivocationReportSystem<Self, Offences, Historical, ReportLongevity>;
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<Runtime>;
type SendMessage = SlashesSendAdapter;

View file

@ -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 ═══════════════════════╝
}
}

View file

@ -1,5 +1,5 @@
{
"version": "0.1.0-autogenerated.13357056092938763018",
"version": "0.1.0-autogenerated.18296316742446681711",
"name": "@polkadot-api/descriptors",
"files": [
"dist"

Binary file not shown.

View file

@ -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);
});