Repository: informalsystems/tendermint-rs (language: rust)

Issue:
Serialization for `validator::Set` and related proto structs is inconsistent, lacks validation
#1340 attempted to fix an issue with deserializing `ValidatorSet` data without the `total_voting_power` field being present in the serialized message.
The field is actually redundant, its value is computable as the sum of voting powers of the validators listed in the message. If the field is not present in the deserialized JSON or protobuf, its value should be recomputed. There is also an issue of trust with impications on protocol security: if we accept the total voting power value without checking it against the sum of listed validators' powers, this means `validator::Set` cannot be assumed correct by construction.

In the serialization bolted onto the tendermint-proto types generated for `ValidatorSet`, we skipped the field to match the serialization schema of misbehavior evidence, where it is not present in at least one place, in #1292.

Unfortunately, `validator::Set` has also had `Serialize` and `Deserialize` derived, the `total_voting_power` field included with no fallback, and this stricter deserialization schema has probably been in use by parties unknown. That serialization is simply derived, with no validations.

### Definition of "done"

The following needs to be implemented and backed by tests:

* In `tendermint`, the `Deserialize` impl for `validator::Set` can tolerate the absence of the `total_voting_power` field by computing it from validator data.
* If the the `total_voting_power` field is present in the deserialized structure, its value is validated against the computed sum of validators' voting powers.
  * A special case can be made for the value of 0, which is easy to fill by default in Go and seems to occur in the wild. This is also the default deserialization for the missing field in protobuf. It should be treated like omission of the field.
* In `tendermint-proto`, the deserialization for the generated `ValidatorSet` structs accepts a present `total_voting_power` field and ~applies the same validation as described above~ represents its value in the struct field. On serialization, the field is omitted to maintain backward compatibility; alternatively (in a technically breaking change), the field is included, but serializations of compound types with validator set fields may bypass this serialization by e.g. using a custom helper.
* The sum computation is protected against integer overflows.
* Validator set serializations where the total voting power exceeds the protocol-defined maximum limit (`u64::MAX / 8`) are rejected as invalid.

_Originally posted by @mzabaluev in https://github.com/informalsystems/tendermint-rs/issues/1340#issuecomment-1708508934_
            


Discussion / hints:

Fix patch:
diff --git a/.changelog/unreleased/bug-fixes/1348-valset-deserialization-fixes.md b/.changelog/unreleased/bug-fixes/1348-valset-deserialization-fixes.md
new file mode 100644
index 000000000..615074d16
--- /dev/null
+++ b/.changelog/unreleased/bug-fixes/1348-valset-deserialization-fixes.md
@@ -0,0 +1,3 @@
+- `[tendermint]` Integer overflows are prevented when calculating the total
+  voting power value in `validator::Set`
+  ([\#1348](https://github.com/informalsystems/tendermint-rs/issues/1348)).
diff --git a/.changelog/unreleased/bug-fixes/1350-pubkey-serde-fixes.md b/.changelog/unreleased/bug-fixes/1350-pubkey-serde-fixes.md
new file mode 100644
index 000000000..725010339
--- /dev/null
+++ b/.changelog/unreleased/bug-fixes/1350-pubkey-serde-fixes.md
@@ -0,0 +1,4 @@
+- `[tendermint-proto]` `Serialize` and `Deserialize` impls for
+  `v*::crypto::PublicKey` are corrected to match the JSON schema used by
+  other implementations
+  ([\#1350](https://github.com/informalsystems/tendermint-rs/pull/1350)).
diff --git a/.changelog/unreleased/improvements/1348-valset-deserialization-fixes.md b/.changelog/unreleased/improvements/1348-valset-deserialization-fixes.md
new file mode 100644
index 000000000..cf91ae114
--- /dev/null
+++ b/.changelog/unreleased/improvements/1348-valset-deserialization-fixes.md
@@ -0,0 +1,12 @@
+- `[tendermint]` Improve and validate deserialization of `validator::Set`
+  ([\#1348](https://github.com/informalsystems/tendermint-rs/issues/1348)).
+  The `total_voting_power` field no longer has to be present in the format
+  processed by `Deserialize`. If it is present, it is validated against the
+  sum of the `voting_power` values of the listed validators. The sum value
+  is also checked against the protocol-defined maximum.
+- `[tendermint-proto]` In the `Deserialize` impls derived for
+  `v*::types::ValidatorSet`, the `total_voting_power` field value is retrieved
+  when present.
+- `[tendermint-proto]` Add serialziation helper module
+  `serializers::from_str_allow_null`. Use it to allow the `proposed_priority`
+  field value of null in the deserialization of `v*::types::Validator`.
diff --git a/proto/src/prost/v0_34/tendermint.crypto.rs b/proto/src/prost/v0_34/tendermint.crypto.rs
index a6273013b..cfd024d19 100644
--- a/proto/src/prost/v0_34/tendermint.crypto.rs
+++ b/proto/src/prost/v0_34/tendermint.crypto.rs
@@ -56,7 +56,6 @@ pub struct ProofOps {
     pub ops: ::prost::alloc::vec::Vec<ProofOp>,
 }
 /// PublicKey defines the keys available for use with Validators
-#[derive(::serde::Deserialize, ::serde::Serialize)]
 #[allow(clippy::derive_partial_eq_without_eq)]
 #[derive(Clone, PartialEq, ::prost::Message)]
 pub struct PublicKey {
diff --git a/proto/src/prost/v0_34/tendermint.types.rs b/proto/src/prost/v0_34/tendermint.types.rs
index c6e153fb3..d33c44a08 100644
--- a/proto/src/prost/v0_34/tendermint.types.rs
+++ b/proto/src/prost/v0_34/tendermint.types.rs
@@ -7,7 +7,8 @@ pub struct ValidatorSet {
     #[prost(message, optional, tag = "2")]
     pub proposer: ::core::option::Option<Validator>,
     #[prost(int64, tag = "3")]
-    #[serde(skip)]
+    #[serde(with = "crate::serializers::from_str", default)]
+    #[serde(skip_serializing)]
     pub total_voting_power: i64,
 }
 #[derive(::serde::Deserialize, ::serde::Serialize)]
@@ -23,7 +24,8 @@ pub struct Validator {
     #[serde(alias = "power", with = "crate::serializers::from_str")]
     pub voting_power: i64,
     #[prost(int64, tag = "4")]
-    #[serde(with = "crate::serializers::from_str", default)]
+    #[serde(with = "crate::serializers::from_str_allow_null")]
+    #[serde(default)]
     pub proposer_priority: i64,
 }
 #[allow(clippy::derive_partial_eq_without_eq)]
diff --git a/proto/src/prost/v0_37/tendermint.crypto.rs b/proto/src/prost/v0_37/tendermint.crypto.rs
index a6273013b..cfd024d19 100644
--- a/proto/src/prost/v0_37/tendermint.crypto.rs
+++ b/proto/src/prost/v0_37/tendermint.crypto.rs
@@ -56,7 +56,6 @@ pub struct ProofOps {
     pub ops: ::prost::alloc::vec::Vec<ProofOp>,
 }
 /// PublicKey defines the keys available for use with Validators
-#[derive(::serde::Deserialize, ::serde::Serialize)]
 #[allow(clippy::derive_partial_eq_without_eq)]
 #[derive(Clone, PartialEq, ::prost::Message)]
 pub struct PublicKey {
diff --git a/proto/src/prost/v0_37/tendermint.types.rs b/proto/src/prost/v0_37/tendermint.types.rs
index 9ec028f2b..7d2145ef6 100644
--- a/proto/src/prost/v0_37/tendermint.types.rs
+++ b/proto/src/prost/v0_37/tendermint.types.rs
@@ -7,7 +7,8 @@ pub struct ValidatorSet {
     #[prost(message, optional, tag = "2")]
     pub proposer: ::core::option::Option<Validator>,
     #[prost(int64, tag = "3")]
-    #[serde(skip)]
+    #[serde(with = "crate::serializers::from_str", default)]
+    #[serde(skip_serializing)]
     pub total_voting_power: i64,
 }
 #[derive(::serde::Deserialize, ::serde::Serialize)]
@@ -23,7 +24,8 @@ pub struct Validator {
     #[serde(alias = "power", with = "crate::serializers::from_str")]
     pub voting_power: i64,
     #[prost(int64, tag = "4")]
-    #[serde(with = "crate::serializers::from_str", default)]
+    #[serde(with = "crate::serializers::from_str_allow_null")]
+    #[serde(default)]
     pub proposer_priority: i64,
 }
 #[allow(clippy::derive_partial_eq_without_eq)]
diff --git a/proto/src/prost/v0_38/tendermint.crypto.rs b/proto/src/prost/v0_38/tendermint.crypto.rs
index a6273013b..cfd024d19 100644
--- a/proto/src/prost/v0_38/tendermint.crypto.rs
+++ b/proto/src/prost/v0_38/tendermint.crypto.rs
@@ -56,7 +56,6 @@ pub struct ProofOps {
     pub ops: ::prost::alloc::vec::Vec<ProofOp>,
 }
 /// PublicKey defines the keys available for use with Validators
-#[derive(::serde::Deserialize, ::serde::Serialize)]
 #[allow(clippy::derive_partial_eq_without_eq)]
 #[derive(Clone, PartialEq, ::prost::Message)]
 pub struct PublicKey {
diff --git a/proto/src/prost/v0_38/tendermint.types.rs b/proto/src/prost/v0_38/tendermint.types.rs
index 2d9bca00f..52a3a5d2e 100644
--- a/proto/src/prost/v0_38/tendermint.types.rs
+++ b/proto/src/prost/v0_38/tendermint.types.rs
@@ -104,7 +104,8 @@ pub struct ValidatorSet {
     #[prost(message, optional, tag = "2")]
     pub proposer: ::core::option::Option<Validator>,
     #[prost(int64, tag = "3")]
-    #[serde(skip)]
+    #[serde(with = "crate::serializers::from_str", default)]
+    #[serde(skip_serializing)]
     pub total_voting_power: i64,
 }
 #[derive(::serde::Deserialize, ::serde::Serialize)]
@@ -120,7 +121,8 @@ pub struct Validator {
     #[serde(alias = "power", with = "crate::serializers::from_str")]
     pub voting_power: i64,
     #[prost(int64, tag = "4")]
-    #[serde(with = "crate::serializers::from_str", default)]
+    #[serde(with = "crate::serializers::from_str_allow_null")]
+    #[serde(default)]
     pub proposer_priority: i64,
 }
 #[allow(clippy::derive_partial_eq_without_eq)]
diff --git a/proto/src/serializers.rs b/proto/src/serializers.rs
index 1f9ffd94a..c4db03f49 100644
--- a/proto/src/serializers.rs
+++ b/proto/src/serializers.rs
@@ -57,6 +57,7 @@ pub mod allow_null;
 pub mod bytes;
 pub mod evidence;
 pub mod from_str;
+pub mod from_str_allow_null;
 pub mod nullable;
 pub mod optional;
 pub mod optional_from_str;
@@ -64,3 +65,5 @@ pub mod part_set_header_total;
 pub mod time_duration;
 pub mod timestamp;
 pub mod txs;
+
+mod public_key;
diff --git a/proto/src/serializers/from_str_allow_null.rs b/proto/src/serializers/from_str_allow_null.rs
new file mode 100644
index 000000000..aaa5598b2
--- /dev/null
+++ b/proto/src/serializers/from_str_allow_null.rs
@@ -0,0 +1,42 @@
+//! Combines [`from_str`] and [`allow_null`].
+//!
+//! Use this module to serialize and deserialize any `T` where `T` implements
+//! [`FromStr`] and [`Display`] to convert from or into a string.
+//! The serialized form is that of `Option<String>`,
+//! and a nil is deserialized to the `Default` value. For JSON, this means both
+//! quoted string values and `null` are accepted. A value is always serialized
+//! as `Some<String>`.
+//! Note that this can be used for all primitive data types.
+//!
+//! [`from_str`]: super::from_str
+//! [`allow_null`]: super::allow_null
+
+use alloc::borrow::Cow;
+use core::fmt::Display;
+use core::str::FromStr;
+
+use serde::{de::Error as _, Deserialize, Deserializer, Serializer};
+
+use crate::prelude::*;
+
+/// Deserialize a nullable string into T
+pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
+where
+    D: Deserializer<'de>,
+    T: FromStr + Default,
+    <T as FromStr>::Err: Display,
+{
+    match <Option<Cow<'_, str>>>::deserialize(deserializer)? {
+        Some(s) => s.parse::<T>().map_err(D::Error::custom),
+        None => Ok(T::default()),
+    }
+}
+
+/// Serialize from T into string
+pub fn serialize<S, T>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
+where
+    S: Serializer,
+    T: Display,
+{
+    serializer.serialize_some(&value.to_string())
+}
diff --git a/proto/src/serializers/public_key.rs b/proto/src/serializers/public_key.rs
new file mode 100644
index 000000000..1c1c47d72
--- /dev/null
+++ b/proto/src/serializers/public_key.rs
@@ -0,0 +1,71 @@
+mod v0_34 {
+    use crate::v0_34::crypto::{public_key, PublicKey};
+    use serde::{Deserialize, Deserializer, Serialize, Serializer};
+
+    impl<'de> Deserialize<'de> for PublicKey {
+        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+        where
+            D: Deserializer<'de>,
+        {
+            let sum = Option::<public_key::Sum>::deserialize(deserializer)?;
+            Ok(Self { sum })
+        }
+    }
+
+    impl Serialize for PublicKey {
+        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+        where
+            S: Serializer,
+        {
+            self.sum.serialize(serializer)
+        }
+    }
+}
+
+mod v0_37 {
+    use crate::v0_37::crypto::{public_key, PublicKey};
+    use serde::{Deserialize, Deserializer, Serialize, Serializer};
+
+    impl<'de> Deserialize<'de> for PublicKey {
+        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+        where
+            D: Deserializer<'de>,
+        {
+            let sum = Option::<public_key::Sum>::deserialize(deserializer)?;
+            Ok(Self { sum })
+        }
+    }
+
+    impl Serialize for PublicKey {
+        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+        where
+            S: Serializer,
+        {
+            self.sum.serialize(serializer)
+        }
+    }
+}
+
+mod v0_38 {
+    use crate::v0_38::crypto::{public_key, PublicKey};
+    use serde::{Deserialize, Deserializer, Serialize, Serializer};
+
+    impl<'de> Deserialize<'de> for PublicKey {
+        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+        where
+            D: Deserializer<'de>,
+        {
+            let sum = Option::<public_key::Sum>::deserialize(deserializer)?;
+            Ok(Self { sum })
+        }
+    }
+
+    impl Serialize for PublicKey {
+        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+        where
+            S: Serializer,
+        {
+            self.sum.serialize(serializer)
+        }
+    }
+}
diff --git a/tendermint/src/error.rs b/tendermint/src/error.rs
index 6ee216c9a..8fcf6294e 100644
--- a/tendermint/src/error.rs
+++ b/tendermint/src/error.rs
@@ -16,7 +16,7 @@ define_error! {
 
         InvalidKey
             { detail: String }
-            |e| { format_args!("invalid key: {e}") },
+            |e| { format_args!("invalid key: {}", e.detail) },
 
         Length
             |_| { format_args!("length error") },
@@ -229,6 +229,12 @@ define_error! {
         NegativeProofIndex
             [ DisplayOnly<TryFromIntError> ]
             |_| { "negative item index in proof" },
+
+        TotalVotingPowerMismatch
+            |_| { "total voting power in validator set does not match the sum of participants' powers" },
+
+        TotalVotingPowerOverflow
+            |_| { "total voting power in validator set exceeds the allowed maximum" },
     }
 }
 
diff --git a/tendermint/src/validator.rs b/tendermint/src/validator.rs
index 96499732f..7db1a7cef 100644
--- a/tendermint/src/validator.rs
+++ b/tendermint/src/validator.rs
@@ -1,7 +1,9 @@
 //! Tendermint validators
 
 use serde::{Deserialize, Serialize};
-use tendermint_proto::v0_37::types::SimpleValidator as RawSimpleValidator;
+use tendermint_proto::v0_38::types::{
+    SimpleValidator as RawSimpleValidator, ValidatorSet as RawValidatorSet,
+};
 use tendermint_proto::Protobuf;
 
 use crate::{
@@ -17,6 +19,7 @@ use crate::{
 
 /// Validator set contains a vector of validators
 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(try_from = "RawValidatorSet")]
 pub struct Set {
     validators: Vec<Info>,
     proposer: Option<Info>,
@@ -24,23 +27,47 @@ pub struct Set {
 }
 
 impl Set {
+    pub const MAX_TOTAL_VOTING_POWER: u64 = (i64::MAX / 8) as u64;
+
     /// Constructor
-    pub fn new(mut validators: Vec<Info>, proposer: Option<Info>) -> Set {
-        Self::sort_validators(&mut validators);
+    pub fn new(validators: Vec<Info>, proposer: Option<Info>) -> Set {
+        Self::try_from_parts(validators, proposer, 0).unwrap()
+    }
 
+    fn try_from_parts(
+        mut validators: Vec<Info>,
+        proposer: Option<Info>,
+        unvalidated_total_voting_power: i64,
+    ) -> Result<Set, Error> {
         // Compute the total voting power
         let total_voting_power = validators
             .iter()
             .map(|v| v.power.value())
-            .sum::<u64>()
-            .try_into()
-            .unwrap();
+            .fold(0u64, |acc, v| acc.saturating_add(v));
+
+        if total_voting_power > Self::MAX_TOTAL_VOTING_POWER {
+            return Err(Error::total_voting_power_overflow());
+        }
+
+        // The conversion cannot fail as we have validated against a smaller limit.
+        let total_voting_power: vote::Power = total_voting_power.try_into().unwrap();
+
+        // If the given total voting power is not the default value,
+        // validate it against the sum of voting powers of the participants.
+        if unvalidated_total_voting_power != 0 {
+            let given_val: vote::Power = unvalidated_total_voting_power.try_into()?;
+            if given_val != total_voting_power {
+                return Err(Error::total_voting_power_mismatch());
+            }
+        }
+
+        Self::sort_validators(&mut validators);
 
-        Set {
+        Ok(Set {
             validators,
             proposer,
             total_voting_power,
-        }
+        })
     }
 
     /// Convenience constructor for cases where there is no proposer
@@ -266,9 +293,8 @@ tendermint_pb_modules! {
                 .collect::<Result<Vec<_>, _>>()?;
 
             let proposer = value.proposer.map(TryInto::try_into).transpose()?;
-            let validator_set = Self::new(validators, proposer);
 
-            Ok(validator_set)
+            Self::try_from_parts(validators, proposer, value.total_voting_power)
         }
     }
 
@@ -481,4 +507,218 @@ mod tests {
         assert_eq!(u64::from(update1.power), 573929);
         assert_eq!(update1, update2);
     }
+
+    #[test]
+    fn validator_set_deserialize_all_fields() {
+        const VSET: &str = r#"{
+            "validators": [
+                {
+                    "address": "01F527D77D3FFCC4FCFF2DDC2952EEA5414F2A33",
+                    "pub_key": {
+                        "type": "tendermint/PubKeyEd25519",
+                        "value": "OAaNq3DX/15fGJP2MI6bujt1GRpvjwrqIevChirJsbc="
+                    },
+                    "voting_power": "50",
+                    "proposer_priority": "-150"
+                },
+                {
+                    "address": "026CC7B6F3E62F789DBECEC59766888B5464737D",
+                    "pub_key": {
+                        "type": "tendermint/PubKeyEd25519",
+                        "value": "+vlsKpn6ojn+UoTZl+w+fxeqm6xvUfBokTcKfcG3au4="
+                    },
+                    "voting_power": "42",
+                    "proposer_priority": "50"
+                }
+            ],
+            "proposer": {
+                "address": "01F527D77D3FFCC4FCFF2DDC2952EEA5414F2A33",
+                "pub_key": {
+                    "type": "tendermint/PubKeyEd25519",
+                    "value": "OAaNq3DX/15fGJP2MI6bujt1GRpvjwrqIevChirJsbc="
+                },
+                "voting_power": "50",
+                "proposer_priority": "-150"
+            },
+            "total_voting_power": "92"
+        }"#;
+
+        let vset = serde_json::from_str::<Set>(VSET).unwrap();
+        assert_eq!(vset.total_voting_power().value(), 92);
+    }
+
+    #[test]
+    fn validator_set_deserialize_no_total_voting_power() {
+        const VSET: &str = r#"{
+            "validators": [
+                {
+                    "address": "01F527D77D3FFCC4FCFF2DDC2952EEA5414F2A33",
+                    "pub_key": {
+                        "type": "tendermint/PubKeyEd25519",
+                        "value": "OAaNq3DX/15fGJP2MI6bujt1GRpvjwrqIevChirJsbc="
+                    },
+                    "voting_power": "50",
+                    "proposer_priority": "-150"
+                },
+                {
+                    "address": "026CC7B6F3E62F789DBECEC59766888B5464737D",
+                    "pub_key": {
+                        "type": "tendermint/PubKeyEd25519",
+                        "value": "+vlsKpn6ojn+UoTZl+w+fxeqm6xvUfBokTcKfcG3au4="
+                    },
+                    "voting_power": "42",
+                    "proposer_priority": "50"
+                }
+            ],
+            "proposer": {
+                "address": "01F527D77D3FFCC4FCFF2DDC2952EEA5414F2A33",
+                "pub_key": {
+                    "type": "tendermint/PubKeyEd25519",
+                    "value": "OAaNq3DX/15fGJP2MI6bujt1GRpvjwrqIevChirJsbc="
+                },
+                "voting_power": "50",
+                "proposer_priority": "-150"
+            }
+        }"#;
+
+        let vset = serde_json::from_str::<Set>(VSET).unwrap();
+        assert_eq!(vset.total_voting_power().value(), 92);
+    }
+
+    #[test]
+    fn validator_set_deserialize_total_voting_power_mismatch() {
+        const VSET: &str = r#"{
+            "validators": [
+                {
+                    "address": "01F527D77D3FFCC4FCFF2DDC2952EEA5414F2A33",
+                    "pub_key": {
+                        "type": "tendermint/PubKeyEd25519",
+                        "value": "OAaNq3DX/15fGJP2MI6bujt1GRpvjwrqIevChirJsbc="
+                    },
+                    "voting_power": "50",
+                    "proposer_priority": "-150"
+                },
+                {
+                    "address": "026CC7B6F3E62F789DBECEC59766888B5464737D",
+                    "pub_key": {
+                        "type": "tendermint/PubKeyEd25519",
+                        "value": "+vlsKpn6ojn+UoTZl+w+fxeqm6xvUfBokTcKfcG3au4="
+                    },
+                    "voting_power": "42",
+                    "proposer_priority": "50"
+                }
+            ],
+            "proposer": {
+                "address": "01F527D77D3FFCC4FCFF2DDC2952EEA5414F2A33",
+                "pub_key": {
+                    "type": "tendermint/PubKeyEd25519",
+                    "value": "OAaNq3DX/15fGJP2MI6bujt1GRpvjwrqIevChirJsbc="
+                },
+                "voting_power": "50",
+                "proposer_priority": "-150"
+            },
+            "total_voting_power": "100"
+        }"#;
+
+        let err = serde_json::from_str::<Set>(VSET).unwrap_err();
+        assert!(
+            err.to_string()
+                .contains("total voting power in validator set does not match the sum of participants' powers"),
+            "{err}"
+        );
+    }
+
+    #[test]
+    fn validator_set_deserialize_total_voting_power_exceeds_limit() {
+        const VSET: &str = r#"{
+            "validators": [
+                {
+                    "address": "01F527D77D3FFCC4FCFF2DDC2952EEA5414F2A33",
+                    "pub_key": {
+                        "type": "tendermint/PubKeyEd25519",
+                        "value": "OAaNq3DX/15fGJP2MI6bujt1GRpvjwrqIevChirJsbc="
+                    },
+                    "voting_power": "576460752303423488",
+                    "proposer_priority": "-150"
+                },
+                {
+                    "address": "026CC7B6F3E62F789DBECEC59766888B5464737D",
+                    "pub_key": {
+                        "type": "tendermint/PubKeyEd25519",
+                        "value": "+vlsKpn6ojn+UoTZl+w+fxeqm6xvUfBokTcKfcG3au4="
+                    },
+                    "voting_power": "576460752303423488",
+                    "proposer_priority": "50"
+                }
+            ],
+            "proposer": {
+                "address": "01F527D77D3FFCC4FCFF2DDC2952EEA5414F2A33",
+                "pub_key": {
+                    "type": "tendermint/PubKeyEd25519",
+                    "value": "OAaNq3DX/15fGJP2MI6bujt1GRpvjwrqIevChirJsbc="
+                },
+                "voting_power": "50",
+                "proposer_priority": "-150"
+            },
+            "total_voting_power": "92"
+        }"#;
+
+        let err = serde_json::from_str::<Set>(VSET).unwrap_err();
+        assert!(
+            err.to_string()
+                .contains("total voting power in validator set exceeds the allowed maximum"),
+            "{err}"
+        );
+    }
+
+    #[test]
+    fn validator_set_deserialize_total_voting_power_overflow() {
+        const VSET: &str = r#"{
+            "validators": [
+                {
+                    "address": "01F527D77D3FFCC4FCFF2DDC2952EEA5414F2A33",
+                    "pub_key": {
+                        "type": "tendermint/PubKeyEd25519",
+                        "value": "OAaNq3DX/15fGJP2MI6bujt1GRpvjwrqIevChirJsbc="
+                    },
+                    "voting_power": "6148914691236517205",
+                    "proposer_priority": "-150"
+                },
+                {
+                    "address": "026CC7B6F3E62F789DBECEC59766888B5464737D",
+                    "pub_key": {
+                        "type": "tendermint/PubKeyEd25519",
+                        "value": "+vlsKpn6ojn+UoTZl+w+fxeqm6xvUfBokTcKfcG3au4="
+                    },
+                    "voting_power": "6148914691236517205",
+                    "proposer_priority": "50"
+                },
+                {
+                    "address": "044EB1BB5D4C1CDB90029648439AEB10431FF295",
+                    "pub_key": {
+                        "type": "tendermint/PubKeyEd25519",
+                        "value": "Wc790fkCDAi7LvZ4UIBAIJSNI+Rp2aU80/8l+idZ/wI="
+                    },
+                    "voting_power": "6148914691236517206",
+                    "proposer_priority": "50"
+                }
+            ],
+            "proposer": {
+                "address": "01F527D77D3FFCC4FCFF2DDC2952EEA5414F2A33",
+                "pub_key": {
+                    "type": "tendermint/PubKeyEd25519",
+                    "value": "OAaNq3DX/15fGJP2MI6bujt1GRpvjwrqIevChirJsbc="
+                },
+                "voting_power": "50",
+                "proposer_priority": "-150"
+            }
+        }"#;
+
+        let err = serde_json::from_str::<Set>(VSET).unwrap_err();
+        assert!(
+            err.to_string()
+                .contains("total voting power in validator set exceeds the allowed maximum"),
+            "{err}"
+        );
+    }
 }
diff --git a/tools/proto-compiler/src/constants.rs b/tools/proto-compiler/src/constants.rs
index a6e9b6873..4c150e6b9 100644
--- a/tools/proto-compiler/src/constants.rs
+++ b/tools/proto-compiler/src/constants.rs
@@ -41,13 +41,14 @@ const TYPE_TAG: &str = r#"#[serde(tag = "type", content = "value")]"#;
 /// Predefined custom attributes for field annotations
 const QUOTED: &str = r#"#[serde(with = "crate::serializers::from_str")]"#;
 const QUOTED_WITH_DEFAULT: &str = r#"#[serde(with = "crate::serializers::from_str", default)]"#;
+const QUOTED_ALLOW_NULL: &str = r#"#[serde(with = "crate::serializers::from_str_allow_null")]"#;
 const DEFAULT: &str = r#"#[serde(default)]"#;
 const HEXSTRING: &str = r#"#[serde(with = "crate::serializers::bytes::hexstring")]"#;
 const BASE64STRING: &str = r#"#[serde(with = "crate::serializers::bytes::base64string")]"#;
 const VEC_BASE64STRING: &str = r#"#[serde(with = "crate::serializers::bytes::vec_base64string")]"#;
 const OPTIONAL: &str = r#"#[serde(with = "crate::serializers::optional")]"#;
 const BYTES_SKIP_IF_EMPTY: &str = r#"#[serde(skip_serializing_if = "bytes::Bytes::is_empty")]"#;
-const SKIP: &str = "#[serde(skip)]";
+const SKIP_SERIALIZING: &str = "#[serde(skip_serializing)]";
 const RENAME_ALL_PASCALCASE: &str = r#"#[serde(rename_all = "PascalCase")]"#;
 const NULLABLEVECARRAY: &str = r#"#[serde(with = "crate::serializers::txs")]"#;
 const NULLABLE: &str = r#"#[serde(with = "crate::serializers::nullable")]"#;
@@ -98,7 +99,7 @@ pub static CUSTOM_TYPE_ATTRIBUTES: &[(&str, &str)] = &[
     (".tendermint.types.Commit", SERIALIZED),
     (".tendermint.types.CommitSig", SERIALIZED),
     (".tendermint.types.ValidatorSet", SERIALIZED),
-    (".tendermint.crypto.PublicKey", SERIALIZED),
+    (".tendermint.crypto.PublicKey.sum", SERIALIZED),
     (".tendermint.crypto.PublicKey.sum", TYPE_TAG),
     (".tendermint.abci.ResponseInfo", SERIALIZED),
     (".tendermint.types.CanonicalBlockID", SERIALIZED),
@@ -197,9 +198,17 @@ pub static CUSTOM_FIELD_ATTRIBUTES: &[(&str, &str)] = &[
     ), // https://github.com/tendermint/tendermint/issues/5549
     (
         ".tendermint.types.Validator.proposer_priority",
+        QUOTED_ALLOW_NULL,
+    ), // null occurs in some LightBlock data
+    (".tendermint.types.Validator.proposer_priority", DEFAULT), // Default is for /genesis deserialization
+    (
+        ".tendermint.types.ValidatorSet.total_voting_power",
         QUOTED_WITH_DEFAULT,
-    ), // Default is for /genesis deserialization
-    (".tendermint.types.ValidatorSet.total_voting_power", SKIP),
+    ),
+    (
+        ".tendermint.types.ValidatorSet.total_voting_power",
+        SKIP_SERIALIZING,
+    ),
     (".tendermint.types.BlockMeta.block_size", QUOTED),
     (".tendermint.types.BlockMeta.num_txs", QUOTED),
     (".tendermint.crypto.PublicKey.sum.ed25519", RENAME_EDPUBKEY),


Test patch: