Repository: microsoft/electionguard-python

Issue:
CompactSubmittedBallot fails to preserve crypto_hash on roundtrip to SubmittedBallot
1. in test_compact_submitted_ballot, line 91

        self.assertEqual(self.submitted_ballot, expanded_ballot)

passes, but 

        self.assertEqual(self.submitted_ballot.crypto_hash, expanded_ballot.crypto_hash)

fails. Not sure why line 91 succeeds, what are the semantics of self.assertEqual?

2. Problem is that the ballot_nonce field in CompactSubmittedBallot is the random_master_nonce of Encrypt.encrypt_ballot():

         random_master_nonce = get_or_else_optional_func(nonce, lambda: rand_q())

but we need the nonce seed:

```
nonce_seed = CiphertextBallot.nonce_seed(
        internal_manifest.manifest_hash,
        ballot.object_id,
        random_master_nonce,
    )
```

3. One way to fix this is to regenerate the nonce_seed in expand_compact_submitted_ballot() in ballot_compact.py:

```
    contests = get_optional(
        encrypt_ballot_contests(
            plaintext_ballot, internal_manifest, context, compact_ballot.ballot_nonce
        )
    )
```
could be:
```
    ElementModQ nonce_seed = CiphertextBallot.nonce_seed(
            internal_manifest.manifest.crypto_hash,
            compact_ballot.compact_plaintext_ballot.object_id,
            compact_ballot.ballot_nonce);

    List<CiphertextBallot.Contest> contests =
            Encrypt.encrypt_ballot_contests(
                    plaintext_ballot, internal_manifest, context, nonce_seed).orElseThrow();
```
(sorry thats the Java, but you get the idea)

Discussion:

Patch:
diff --git a/src/electionguard/ballot_compact.py b/src/electionguard/ballot_compact.py
index 6505172..b2f6892 100644
--- a/src/electionguard/ballot_compact.py
+++ b/src/electionguard/ballot_compact.py
@@ -3,6 +3,7 @@ from typing import Dict, List
 
 
 from .ballot import (
+    CiphertextBallot,
     SubmittedBallot,
     ExtendedData,
     PlaintextBallot,
@@ -85,9 +86,14 @@ def expand_compact_submitted_ballot(
     plaintext_ballot = expand_compact_plaintext_ballot(
         compact_ballot.compact_plaintext_ballot, internal_manifest
     )
+    nonce_seed = CiphertextBallot.nonce_seed(
+        internal_manifest.manifest_hash,
+        compact_ballot.compact_plaintext_ballot.object_id,
+        compact_ballot.ballot_nonce,
+    )
     contests = get_optional(
         encrypt_ballot_contests(
-            plaintext_ballot, internal_manifest, context, compact_ballot.ballot_nonce
+            plaintext_ballot, internal_manifest, context, nonce_seed
         )
     )
     crypto_hash = create_ballot_hash(


Test patch:
diff --git a/tests/unit/test_ballot_compact.py b/tests/unit/test_ballot_compact.py
index a6cc044..8480289 100644
--- a/tests/unit/test_ballot_compact.py
+++ b/tests/unit/test_ballot_compact.py
@@ -88,3 +88,4 @@ class TestCompactBallot(TestCase):
         # Assert
         self.assertIsNotNone(expanded_ballot)
         self.assertEqual(self.submitted_ballot, expanded_ballot)
+        self.assertEqual(self.submitted_ballot.crypto_hash, expanded_ballot.crypto_hash)