Repository: microsoft/electionguard-python

Issue:
🐞 Deserializing SubmittedBallot makes ElgamalCiphertext comparison fail
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

Since the new serialization has been merged, deserialization fails to reconstruct `SubmittedBallot`s

I'm trying to write the `SubmittedBallot`s to file. In order to create the tally, I deserialize the ballots with the `electionguard.serializer:from_file` function.
By doing this, the `ElgamalCiphertext` aka `ciphertext_accumulation` becomes different from the result of `elgamal_accumulate` function thus result in an invalid ballot that does not become part of the tally.

the `ciphertext_accumulation` is of type `__Pydantic_xxxxxxxx_ElgamalCiphertext` and there's no `__eq__` that can handle this by default.

I see there are some `__eq__` functions defined in electionguard-python and they are all like:
```python
def __eq__(self, other):
    return (
        isinstance(other, type)
        and self.x == other.x
        and self.y == other.y
        ...
    )
```

which is the default `__eq__` function that pydantic generates for `dataclasses`

I suggest that all `dataclasses.dataclass` could be replaced by `pydantic.dataclasses.dataclass`

Pydantic handles the builtins `dataclass.field` with the same syntax and that would allow a working deserializing and generate the `__eq__` function so we could remove them.


If not, we can add only the `__eq__` function for the `ElgamalCiphertext` class, that would help in my own case but I'm not sure that it would be very nice to help such a specific use case.

What could I, in my case, if case dataclasses are not replaced, to deserialize my ballots except by recursively deserializing every class involved in such a ballot ?
What would be the point in being able to serialize every single object in electionguard if deserialization don't work as one could expect?

That's why I think electionguard should use pydantic from start to end, not partly.


### Expected Behavior

_No response_

### Steps To Reproduce

use the `electionguard.serialize:to_file` to write a `SubmittedBallot`
use the `electionguard.serialize:from_file` to load a `SubmittedBallot`

compare the ballot's `ciphertext_accumulation` with the result of its `elgamal_accumulate()` method

### Environment

```markdown
- OS:
```


### Anything else?

_No response_

Discussion:
keithrfung: CC @thommignot

Patch:
diff --git a/src/electionguard/elgamal.py b/src/electionguard/elgamal.py
index 3b15778..4ec62fc 100644
--- a/src/electionguard/elgamal.py
+++ b/src/electionguard/elgamal.py
@@ -1,5 +1,6 @@
 from dataclasses import dataclass
-from typing import Iterable, Optional
+from typing import Any, Iterable, Optional
+
 
 from .discrete_log import DiscreteLog
 from .group import (
@@ -43,6 +44,11 @@ class ElGamalCiphertext:
     data: ElementModP
     """encrypted data or beta"""
 
+    def __eq__(self, other: Any) -> bool:
+        if isinstance(other, ElGamalCiphertext):
+            return self.pad == other.pad and self.data == other.data
+        return False
+
     def decrypt_known_product(self, product: ElementModP) -> int:
         """
         Decrypts an ElGamal ciphertext with a "known product" (the blinding factor used in the encryption).


Test patch:
diff --git a/tests/integration/test_end_to_end_election.py b/tests/integration/test_end_to_end_election.py
index 8319193..45062cd 100644
--- a/tests/integration/test_end_to_end_election.py
+++ b/tests/integration/test_end_to_end_election.py
@@ -587,6 +587,13 @@ class TestEndToEndElection(BaseTestCase):
                     submitted_ballots_directory,
                 ),
             )
+            self.assertTrue(
+                ballot_from_file.is_valid_encryption(
+                    self.internal_manifest.manifest_hash,
+                    self.context.elgamal_public_key,
+                    self.context.crypto_extended_base_hash,
+                )
+            )
             self.assertEqualAsDicts(ballot, ballot_from_file)
 
         for spoiled_ballot in self.plaintext_spoiled_ballots.values():