feat(slashing): double-sign fraud proof via nu_reportDoubleSign
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6bc6e90114
commit
d2adf9381a
7 changed files with 370 additions and 3 deletions
|
|
@ -7,6 +7,13 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.12.0] — 2026-04-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `nu-state/slash_evidence.rs` — `SlashEvidence` struct; DB key `slash_evidence:<validator>:<slot>`
|
||||||
|
- `nu_reportDoubleSign` RPC handler — iki farklı block imzasını k256 ile doğrular, geçerliyse DB'ye yazar; idempotent (aynı kanıt tekrar kabul edilmez)
|
||||||
|
- `block_loop::apply_pending_slashes` — her slot'ta uygulanmamış kanıtları işler: `slash_double_sign` çağırır, slashed stake'in %50'si BURN_WALLET'a, %50'si reporter'a akar
|
||||||
|
|
||||||
## [0.11.0] — 2026-04-25
|
## [0.11.0] — 2026-04-25
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
185
Cargo.lock
generated
185
Cargo.lock
generated
|
|
@ -154,12 +154,24 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base16ct"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64ct"
|
||||||
|
version = "1.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bindgen"
|
name = "bindgen"
|
||||||
version = "0.69.5"
|
version = "0.69.5"
|
||||||
|
|
@ -333,6 +345,12 @@ version = "1.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-oid"
|
||||||
|
version = "0.9.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
|
@ -368,6 +386,18 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-bigint"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"rand_core",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
|
|
@ -378,6 +408,16 @@ dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "der"
|
||||||
|
version = "0.7.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||||
|
dependencies = [
|
||||||
|
"const-oid",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
|
|
@ -385,7 +425,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer",
|
"block-buffer",
|
||||||
|
"const-oid",
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -399,12 +441,45 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ecdsa"
|
||||||
|
version = "0.16.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
|
||||||
|
dependencies = [
|
||||||
|
"der",
|
||||||
|
"digest",
|
||||||
|
"elliptic-curve",
|
||||||
|
"rfc6979",
|
||||||
|
"signature",
|
||||||
|
"spki",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.15.0"
|
version = "1.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "elliptic-curve"
|
||||||
|
version = "0.13.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
|
||||||
|
dependencies = [
|
||||||
|
"base16ct",
|
||||||
|
"crypto-bigint",
|
||||||
|
"digest",
|
||||||
|
"ff",
|
||||||
|
"generic-array",
|
||||||
|
"group",
|
||||||
|
"pkcs8",
|
||||||
|
"rand_core",
|
||||||
|
"sec1",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.35"
|
version = "0.8.35"
|
||||||
|
|
@ -436,6 +511,16 @@ version = "2.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ff"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
|
|
@ -519,6 +604,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
"version_check",
|
"version_check",
|
||||||
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -550,6 +636,17 @@ version = "0.3.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "group"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
|
||||||
|
dependencies = [
|
||||||
|
"ff",
|
||||||
|
"rand_core",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
|
|
@ -587,6 +684,15 @@ version = "0.4.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
|
@ -915,6 +1021,20 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "k256"
|
||||||
|
version = "0.13.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"ecdsa",
|
||||||
|
"elliptic-curve",
|
||||||
|
"once_cell",
|
||||||
|
"sha2",
|
||||||
|
"signature",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
|
@ -1157,6 +1277,8 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"hex",
|
||||||
|
"k256",
|
||||||
"nu-block",
|
"nu-block",
|
||||||
"nu-mempool",
|
"nu-mempool",
|
||||||
"nu-state",
|
"nu-state",
|
||||||
|
|
@ -1293,6 +1415,16 @@ version = "0.2.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkcs8"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||||
|
dependencies = [
|
||||||
|
"der",
|
||||||
|
"spki",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.33"
|
version = "0.3.33"
|
||||||
|
|
@ -1332,6 +1464,15 @@ version = "5.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
|
|
@ -1410,6 +1551,16 @@ dependencies = [
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rfc6979"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
|
||||||
|
dependencies = [
|
||||||
|
"hmac",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.14"
|
version = "0.17.14"
|
||||||
|
|
@ -1519,6 +1670,20 @@ version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sec1"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
|
||||||
|
dependencies = [
|
||||||
|
"base16ct",
|
||||||
|
"der",
|
||||||
|
"generic-array",
|
||||||
|
"pkcs8",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "3.7.0"
|
version = "3.7.0"
|
||||||
|
|
@ -1644,6 +1809,16 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signature"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
|
|
@ -1666,6 +1841,16 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spki"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"der",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ anyhow.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
|
k256 = { version = "0.13", features = ["ecdsa"] }
|
||||||
|
hex.workspace = true
|
||||||
nu-state = { path = "../nu-state" }
|
nu-state = { path = "../nu-state" }
|
||||||
nu-mempool = { path = "../nu-mempool" }
|
nu-mempool = { path = "../nu-mempool" }
|
||||||
nu-block = { path = "../nu-block" }
|
nu-block = { path = "../nu-block" }
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ use crate::{
|
||||||
server::AppState,
|
server::AppState,
|
||||||
types::{JsonRpcRequest, JsonRpcResponse},
|
types::{JsonRpcRequest, JsonRpcResponse},
|
||||||
};
|
};
|
||||||
|
use k256::ecdsa::{signature::Verifier, Signature, VerifyingKey};
|
||||||
use nu_block::types::{Block, RawTransaction};
|
use nu_block::types::{Block, RawTransaction};
|
||||||
use nu_state::{account::AccountState, ValidatorState};
|
use nu_state::{account::AccountState, SlashEvidence, ValidatorState};
|
||||||
|
|
||||||
pub async fn dispatch(req: JsonRpcRequest, state: &AppState) -> JsonRpcResponse {
|
pub async fn dispatch(req: JsonRpcRequest, state: &AppState) -> JsonRpcResponse {
|
||||||
match req.method.as_str() {
|
match req.method.as_str() {
|
||||||
|
|
@ -15,6 +16,7 @@ pub async fn dispatch(req: JsonRpcRequest, state: &AppState) -> JsonRpcResponse
|
||||||
"nu_getBlock" => handle_get_block(&req, state).await,
|
"nu_getBlock" => handle_get_block(&req, state).await,
|
||||||
"nu_getValidator" => handle_get_validator(&req, state).await,
|
"nu_getValidator" => handle_get_validator(&req, state).await,
|
||||||
"nu_listValidators" => handle_list_validators(&req, state).await,
|
"nu_listValidators" => handle_list_validators(&req, state).await,
|
||||||
|
"nu_reportDoubleSign" => handle_report_double_sign(&req, state).await,
|
||||||
"nu_getTx" => not_implemented(&req, "nu_getTx"),
|
"nu_getTx" => not_implemented(&req, "nu_getTx"),
|
||||||
"nu_getStory" => not_implemented(&req, "nu_getStory"),
|
"nu_getStory" => not_implemented(&req, "nu_getStory"),
|
||||||
"nu_getNode" => not_implemented(&req, "nu_getNode"),
|
"nu_getNode" => not_implemented(&req, "nu_getNode"),
|
||||||
|
|
@ -137,6 +139,83 @@ async fn handle_list_validators(req: &JsonRpcRequest, state: &AppState) -> JsonR
|
||||||
JsonRpcResponse::ok(req.id.clone(), serde_json::to_value(validators).unwrap())
|
JsonRpcResponse::ok(req.id.clone(), serde_json::to_value(validators).unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_report_double_sign(req: &JsonRpcRequest, state: &AppState) -> JsonRpcResponse {
|
||||||
|
// params: [validator, slot, block_hash_1, sig_1_hex, block_hash_2, sig_2_hex, reporter]
|
||||||
|
let get_str = |i: usize| req.params.get(i).and_then(|v| v.as_str()).map(String::from);
|
||||||
|
let get_u32 = |i: usize| req.params.get(i).and_then(|v| v.as_u64()).map(|n| n as u32);
|
||||||
|
|
||||||
|
let (Some(validator), Some(slot), Some(hash1), Some(sig1_hex), Some(hash2), Some(sig2_hex), Some(reporter)) = (
|
||||||
|
get_str(0), get_u32(1), get_str(2), get_str(3), get_str(4), get_str(5), get_str(6),
|
||||||
|
) else {
|
||||||
|
return JsonRpcResponse::err(req.id.clone(), -32602, "Missing params".into());
|
||||||
|
};
|
||||||
|
|
||||||
|
if hash1 == hash2 {
|
||||||
|
return JsonRpcResponse::err(req.id.clone(), -32602, "Block hashes must differ".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify both signatures are from the same validator key
|
||||||
|
let sig1_bytes = match hex::decode(&sig1_hex) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(_) => return JsonRpcResponse::err(req.id.clone(), -32602, "Invalid sig_1 hex".into()),
|
||||||
|
};
|
||||||
|
let sig2_bytes = match hex::decode(&sig2_hex) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(_) => return JsonRpcResponse::err(req.id.clone(), -32602, "Invalid sig_2 hex".into()),
|
||||||
|
};
|
||||||
|
let val_pubkey_bytes = match hex::decode(&validator) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(_) => return JsonRpcResponse::err(req.id.clone(), -32602, "Invalid validator address".into()),
|
||||||
|
};
|
||||||
|
let verifying_key = match VerifyingKey::from_sec1_bytes(&val_pubkey_bytes) {
|
||||||
|
Ok(k) => k,
|
||||||
|
Err(_) => return JsonRpcResponse::err(req.id.clone(), -32602, "Invalid validator pubkey".into()),
|
||||||
|
};
|
||||||
|
let sig1 = match Signature::from_slice(&sig1_bytes) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return JsonRpcResponse::err(req.id.clone(), -32602, "Invalid sig_1".into()),
|
||||||
|
};
|
||||||
|
let sig2 = match Signature::from_slice(&sig2_bytes) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return JsonRpcResponse::err(req.id.clone(), -32602, "Invalid sig_2".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if verifying_key.verify(hash1.as_bytes(), &sig1).is_err() {
|
||||||
|
return JsonRpcResponse::err(req.id.clone(), -32602, "sig_1 does not verify".into());
|
||||||
|
}
|
||||||
|
if verifying_key.verify(hash2.as_bytes(), &sig2).is_err() {
|
||||||
|
return JsonRpcResponse::err(req.id.clone(), -32602, "sig_2 does not verify".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let evidence = SlashEvidence {
|
||||||
|
validator: validator.clone(),
|
||||||
|
slot,
|
||||||
|
block_hash_1: hash1,
|
||||||
|
sig_1: sig1_bytes,
|
||||||
|
block_hash_2: hash2,
|
||||||
|
sig_2: sig2_bytes,
|
||||||
|
reporter,
|
||||||
|
submitted_at: chrono::Utc::now().timestamp_millis(),
|
||||||
|
applied: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let key = SlashEvidence::db_key(&validator, slot);
|
||||||
|
let db = state.db.lock().await;
|
||||||
|
|
||||||
|
// Idempotent — don't store duplicates
|
||||||
|
if let Ok(Some(_)) = db.get::<SlashEvidence>(&key) {
|
||||||
|
return JsonRpcResponse::ok(req.id.clone(), json!({ "status": "already_reported" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
match db.put(&key, &evidence) {
|
||||||
|
Ok(_) => {
|
||||||
|
tracing::warn!(validator = %validator, slot, "double-sign evidence submitted");
|
||||||
|
JsonRpcResponse::ok(req.id.clone(), json!({ "status": "accepted", "key": key }))
|
||||||
|
}
|
||||||
|
Err(e) => JsonRpcResponse::err(req.id.clone(), -32000, e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn not_implemented(req: &JsonRpcRequest, method: &str) -> JsonRpcResponse {
|
fn not_implemented(req: &JsonRpcRequest, method: &str) -> JsonRpcResponse {
|
||||||
JsonRpcResponse::err(req.id.clone(), -32000, format!("{method} not implemented yet"))
|
JsonRpcResponse::err(req.id.clone(), -32000, format!("{method} not implemented yet"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,13 @@ pub mod accessor;
|
||||||
pub mod account;
|
pub mod account;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod nft;
|
pub mod nft;
|
||||||
|
pub mod slash_evidence;
|
||||||
pub mod story_node;
|
pub mod story_node;
|
||||||
pub mod validator;
|
pub mod validator;
|
||||||
|
|
||||||
pub use accessor::StateAccessor;
|
pub use accessor::StateAccessor;
|
||||||
pub use db::StateDb;
|
pub use db::StateDb;
|
||||||
pub use nft::NftState;
|
pub use nft::NftState;
|
||||||
|
pub use slash_evidence::SlashEvidence;
|
||||||
pub use story_node::{NodeStatus, StoryNodeState, WeightedVote};
|
pub use story_node::{NodeStatus, StoryNodeState, WeightedVote};
|
||||||
pub use validator::ValidatorState;
|
pub use validator::ValidatorState;
|
||||||
|
|
|
||||||
22
crates/nu-state/src/slash_evidence.rs
Normal file
22
crates/nu-state/src/slash_evidence.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Double-sign fraud proof submitted via nu_reportDoubleSign RPC.
|
||||||
|
/// Stored as "slash_evidence:<validator_addr>:<slot>" in DB.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SlashEvidence {
|
||||||
|
pub validator: String,
|
||||||
|
pub slot: u32,
|
||||||
|
pub block_hash_1: String,
|
||||||
|
pub sig_1: Vec<u8>,
|
||||||
|
pub block_hash_2: String,
|
||||||
|
pub sig_2: Vec<u8>,
|
||||||
|
pub reporter: String,
|
||||||
|
pub submitted_at: i64,
|
||||||
|
pub applied: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SlashEvidence {
|
||||||
|
pub fn db_key(validator: &str, slot: u32) -> String {
|
||||||
|
format!("slash_evidence:{validator}:{slot:010}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,12 +9,12 @@ use nu_block::{
|
||||||
};
|
};
|
||||||
use nu_consensus::{
|
use nu_consensus::{
|
||||||
slot::current_slot,
|
slot::current_slot,
|
||||||
slashing::record_skip,
|
slashing::{record_skip, slash_double_sign},
|
||||||
types::ValidatorRecord,
|
types::ValidatorRecord,
|
||||||
validator_set::ValidatorSet,
|
validator_set::ValidatorSet,
|
||||||
};
|
};
|
||||||
use nu_mempool::Mempool;
|
use nu_mempool::Mempool;
|
||||||
use nu_state::{story_node::StoryNodeState, ValidatorState, StateDb};
|
use nu_state::{account::AccountState, story_node::StoryNodeState, SlashEvidence, ValidatorState, StateDb};
|
||||||
use nu_vm::execute_block;
|
use nu_vm::execute_block;
|
||||||
|
|
||||||
use crate::p2p::P2pSender;
|
use crate::p2p::P2pSender;
|
||||||
|
|
@ -86,6 +86,12 @@ pub async fn run(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply any pending double-sign slash evidence
|
||||||
|
{
|
||||||
|
let db_guard = db.lock().await;
|
||||||
|
apply_pending_slashes(&db_guard, slot);
|
||||||
|
}
|
||||||
|
|
||||||
// Scheduler: inject auto-txs for pending nodes
|
// Scheduler: inject auto-txs for pending nodes
|
||||||
{
|
{
|
||||||
let db_guard = db.lock().await;
|
let db_guard = db.lock().await;
|
||||||
|
|
@ -168,6 +174,70 @@ pub async fn run(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_pending_slashes(db: &StateDb, current_slot: u32) {
|
||||||
|
let burn_wallet = std::env::var("BURN_WALLET").unwrap_or_default();
|
||||||
|
let evidences: Vec<SlashEvidence> = db.scan_prefix("slash_evidence:");
|
||||||
|
|
||||||
|
for mut ev in evidences {
|
||||||
|
if ev.applied {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let vs_key = format!("validator:{}", ev.validator);
|
||||||
|
let mut vs = match db.get::<ValidatorState>(&vs_key).ok().flatten() {
|
||||||
|
Some(v) => v,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut record = ValidatorRecord {
|
||||||
|
address: vs.address.clone(),
|
||||||
|
stake: vs.stake,
|
||||||
|
pon_score: vs.pon_score,
|
||||||
|
is_active: vs.is_active,
|
||||||
|
last_block: vs.last_block,
|
||||||
|
slash_count: vs.slash_count,
|
||||||
|
skip_count: vs.skip_count,
|
||||||
|
consecutive_blocks: vs.consecutive_blocks,
|
||||||
|
ban_until_slot: vs.ban_until_slot,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = slash_double_sign(&mut record, current_slot);
|
||||||
|
|
||||||
|
vs.stake = record.stake;
|
||||||
|
vs.pon_score = record.pon_score;
|
||||||
|
vs.is_active = record.is_active;
|
||||||
|
vs.ban_until_slot = record.ban_until_slot;
|
||||||
|
vs.slash_count = record.slash_count;
|
||||||
|
|
||||||
|
let _ = db.put(&vs_key, &vs);
|
||||||
|
|
||||||
|
// Burn half
|
||||||
|
if !burn_wallet.is_empty() {
|
||||||
|
if let Ok(Some(mut burn_acc)) = db.get::<AccountState>(&format!("account:{burn_wallet}")) {
|
||||||
|
burn_acc.balance = burn_acc.balance.saturating_add(result.burn_amount);
|
||||||
|
let _ = db.put(&format!("account:{burn_wallet}"), &burn_acc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reporter reward
|
||||||
|
if !ev.reporter.is_empty() {
|
||||||
|
if let Ok(Some(mut reporter_acc)) = db.get::<AccountState>(&format!("account:{}", ev.reporter)) {
|
||||||
|
reporter_acc.balance = reporter_acc.balance.saturating_add(result.reporter_reward);
|
||||||
|
let _ = db.put(&format!("account:{}", ev.reporter), &reporter_acc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ev.applied = true;
|
||||||
|
let _ = db.put(&SlashEvidence::db_key(&ev.validator, ev.slot), &ev);
|
||||||
|
|
||||||
|
tracing::warn!(
|
||||||
|
validator = %ev.validator,
|
||||||
|
slot = ev.slot,
|
||||||
|
slashed = result.slashed_amount,
|
||||||
|
"double-sign slash applied"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn unban_expired_validators(db: &StateDb, current_slot: u32) {
|
fn unban_expired_validators(db: &StateDb, current_slot: u32) {
|
||||||
let records: Vec<ValidatorState> = db.scan_prefix("validator:");
|
let records: Vec<ValidatorState> = db.scan_prefix("validator:");
|
||||||
for mut vs in records {
|
for mut vs in records {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue