Skip to content

Commit

Permalink
Merge pull request #423 from JohnLCaron/tallyErrors
Browse files Browse the repository at this point in the history
AccumulateTally uses ErrorMessages (no Exceptions).
  • Loading branch information
JohnLCaron authored Nov 5, 2023
2 parents cbf2326 + 4669085 commit 753d429
Show file tree
Hide file tree
Showing 15 changed files with 108 additions and 63 deletions.
13 changes: 3 additions & 10 deletions egklib/src/commonMain/kotlin/electionguard/core/ElGamal.kt
Original file line number Diff line number Diff line change
Expand Up @@ -189,16 +189,9 @@ operator fun ElGamalCiphertext.plus(o: ElGamalCiphertext): ElGamalCiphertext {
* Homomorphically "adds" a sequence of ElGamal ciphertexts through piecewise multiplication.
* @throws ArithmeticException if the sequence is empty
*/
fun Iterable<ElGamalCiphertext>.encryptedSum(): ElGamalCiphertext =
// This operation isn't defined on an empty list -- we'd have to have some way of getting
// an encryption of zero, but we don't have the public key handy -- so we'll just raise
// an exception on that, and otherwise we're fine.
asSequence()
.let {
// TODO why not return null?
it.ifEmpty { throw ArithmeticException("Cannot sum an empty list of ciphertexts") }
.reduce { a, b -> a + b }
}
fun List<ElGamalCiphertext>.encryptedSum(): ElGamalCiphertext? {
return if (this.isEmpty()) null else this.reduce { a, b -> a + b }
}

/** Add two lists by component-wise multiplication */
fun List<ElGamalCiphertext>.add(other: List<ElGamalCiphertext>): List<ElGamalCiphertext> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ fun PlaintextBallot.Contest.encryptContest(
): CiphertextBallot.Contest {

val ciphertexts: List<ElGamalCiphertext> = encryptedSelections.map { it.ciphertext }
val ciphertextAccumulation: ElGamalCiphertext = ciphertexts.encryptedSum()
val ciphertextAccumulation: ElGamalCiphertext = ciphertexts.encryptedSum()?: 0.encrypt(jointPublicKey)
val nonces: Iterable<ElementModQ> = encryptedSelections.map { it.selectionNonce }
val aggNonce: ElementModQ = with(group) { nonces.addQ() }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class Recorder(
val selections = this.makeSelections(preeContest)

val texts: List<ElGamalCiphertext> = selections.map { it.ciphertext }
val ciphertextAccumulation: ElGamalCiphertext = texts.encryptedSum()
val ciphertextAccumulation: ElGamalCiphertext = texts.encryptedSum()?: 0.encrypt(publicKeyEG)
val nonces: Iterable<ElementModQ> = selections.map { it.selectionNonce }
val aggNonce: ElementModQ = with(group) { nonces.addQ() }
val totalVotes = votedFor.map{ if (it) 1 else 0 }.sum()
Expand Down Expand Up @@ -133,7 +133,7 @@ class Recorder(
val combinedEncryption = mutableListOf<ElGamalCiphertext>()
repeat(nselections) { idx ->
val componentEncryptions : List<ElGamalCiphertext> = this.selectedVectors.map { it.encryptions[idx] }
combinedEncryption.add( componentEncryptions.encryptedSum() )
combinedEncryption.add( componentEncryptions.encryptedSum()?: 0.encrypt(publicKeyEG) )
}

// the encryption nonces are added to create suitable nonces
Expand Down
52 changes: 29 additions & 23 deletions egklib/src/commonMain/kotlin/electionguard/tally/AccumulateTally.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,43 @@ import electionguard.ballot.EncryptedBallotIF
import electionguard.ballot.EncryptedTally
import electionguard.ballot.ManifestIF
import electionguard.ballot.EncryptedBallot.BallotState
import electionguard.core.ElGamalCiphertext
import electionguard.core.GroupContext
import electionguard.core.UInt256
import electionguard.core.encryptedSum
import electionguard.core.*
import electionguard.util.ErrorMessages
import io.github.oshai.kotlinlogging.KotlinLogging

private val logger = KotlinLogging.logger("AccumulateTally")

/** Accumulate the votes of EncryptedBallots, and return a new EncryptedTally. */
// TODO what happens if there are no EncryptedBallots?
class AccumulateTally(val group : GroupContext, val manifest : ManifestIF, val name : String, val extendedBaseHash : UInt256) {
private val contests = manifest.contests.associate { it.contestId to Contest(it)}
class AccumulateTally(
val group: GroupContext,
val manifest: ManifestIF,
val name: String,
val extendedBaseHash: UInt256,
val jointPublicKey: ElGamalPublicKey,
) {
private val contests = manifest.contests.associate { it.contestId to Contest(it) }
private val castIds = mutableSetOf<String>()

fun addCastBallot(ballot: EncryptedBallotIF): Boolean {
fun addCastBallot(ballot: EncryptedBallotIF, errs: ErrorMessages): Boolean {
if (ballot.state != BallotState.CAST) {
logger.warn { "Ballot ${ballot.ballotId} does not have state CAST"}
errs.add("Ballot ${ballot.ballotId} does not have state CAST")
return false
}
if (!this.castIds.add(ballot.ballotId)) {
logger.warn { "Ballot ${ballot.ballotId} is duplicate"}
if (ballot.electionId != extendedBaseHash) {
errs.add("Ballot ${ballot.ballotId} has wrong electionId ${ballot.electionId}")
return false
}
if (ballot.electionId != extendedBaseHash) {
logger.warn { "Ballot ${ballot.ballotId} has wrong electionId ${ballot.electionId}"}
if (!this.castIds.add(ballot.ballotId)) {
errs.add("Ballot ${ballot.ballotId} is duplicate")
return false
}

for (ballotContest in ballot.contests) {
val contest = contests[ballotContest.contestId]
if (contest == null) {
logger.warn { "Ballot ${ballot.ballotId} has contest ${ballotContest.contestId} not in manifest"}
errs.add("Ballot ${ballot.ballotId} has contest ${ballotContest.contestId} not in manifest")
} else {
contest.accumulate(ballot.ballotId, ballotContest)
contest.accumulate(ballot.ballotId, ballotContest, errs.nested("Contest ${ballotContest.contestId}"))
}
}
return true
Expand All @@ -48,14 +51,14 @@ class AccumulateTally(val group : GroupContext, val manifest : ManifestIF, val n
return EncryptedTally(this.name, tallyContests, castIds.toList(), extendedBaseHash)
}

private inner class Contest(val manifestContest : ManifestIF.Contest) {
private val selections = manifestContest.selections.associate { it.selectionId to Selection(it)}
private inner class Contest(val manifestContest: ManifestIF.Contest) {
private val selections = manifestContest.selections.associate { it.selectionId to Selection(it) }

fun accumulate(ballotId : String, ballotContest: EncryptedBallotIF.Contest) {
fun accumulate(ballotId: String, ballotContest: EncryptedBallotIF.Contest, errs: ErrorMessages) {
for (ballotSelection in ballotContest.selections) {
val selection = selections[ballotSelection.selectionId]
if (selection == null) {
logger.warn { "Ballot $ballotId has illegal selection ${ballotSelection.selectionId} in contest ${ballotContest.contestId}"}
errs.add("Ballot $ballotId has illegal selection ${ballotSelection.selectionId} in contest ${ballotContest.contestId}")
} else {
selection.accumulate(ballotSelection.encryptedVote)
}
Expand All @@ -65,20 +68,23 @@ class AccumulateTally(val group : GroupContext, val manifest : ManifestIF, val n
fun build(): EncryptedTally.Contest {
val tallySelections = selections.values.map { it.build() }
return EncryptedTally.Contest(
manifestContest.contestId, manifestContest.sequenceOrder, tallySelections)
manifestContest.contestId, manifestContest.sequenceOrder, tallySelections
)
}
}

private inner class Selection(val manifestSelection : ManifestIF.Selection) {
private val ciphertextAccumulate = ArrayList<ElGamalCiphertext>()
private inner class Selection(val manifestSelection: ManifestIF.Selection) {
private val ciphertextAccumulate = mutableListOf<ElGamalCiphertext>()

fun accumulate(selection: ElGamalCiphertext) {
ciphertextAccumulate.add(selection)
}

fun build(): EncryptedTally.Selection {
return EncryptedTally.Selection(
manifestSelection.selectionId, manifestSelection.sequenceOrder, ciphertextAccumulate.encryptedSum(),
manifestSelection.selectionId,
manifestSelection.sequenceOrder,
ciphertextAccumulate.encryptedSum() ?: 0.encrypt(jointPublicKey),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,8 @@ class VerifyEncryptedBallots(
}

// Verification 6 (Adherence to vote limits)
// TODO what happens if there are no selections?
val texts: List<ElGamalCiphertext> = contest.selections.map { it.encryptedVote }
val ciphertextAccumulation: ElGamalCiphertext = texts.encryptedSum()
val ciphertextAccumulation: ElGamalCiphertext = texts.encryptedSum()?: 0.encrypt(jointPublicKey)
val cvalid = contest.proof.verify(
ciphertextAccumulation,
this.jointPublicKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ fun VerifyEncryptedBallots.verifyPreencryptionShortCodes(
// product of multiple pre-encryption selection vectors. component-wise I think
for (idx in 0 until nselection) {
val compList = cv.selectedVectors.map { it.encryptions[idx] }
val sum = compList.encryptedSum()
val sum = compList.encryptedSum()?: 0.encrypt(jointPublicKey)
if (sum != selectionVector[idx]) {
results.add(Err(" 18.B Contest ${contest.contestId} (contestLimit=$contestLimit) selectionVector $idx does not match product"))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ class ChaumPedersenTest {
val nonceAccum = nonce + nonce + nonce // we're using it three times

val texts: List<ElGamalCiphertext> = listOf(vote0, vote1, vote41)
val message: ElGamalCiphertext = texts.encryptedSum()
val message: ElGamalCiphertext = texts.encryptedSum()?: 0.encrypt(publicKey)

val proof =
message.makeChaumPedersen(
Expand Down Expand Up @@ -329,7 +329,7 @@ class ChaumPedersenTest {
val nonceAccum = nonceSequence[0] + nonceSequence[1] + nonceSequence[2]

val texts: List<ElGamalCiphertext> = listOf(vote0, vote1, vote41)
val ciphertextAccumulation: ElGamalCiphertext = texts.encryptedSum()
val ciphertextAccumulation: ElGamalCiphertext = texts.encryptedSum()?: 0.encrypt(publicKey)

val proof =
ciphertextAccumulation.makeChaumPedersen(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
package electionguard.tally

import com.github.michaelbull.result.Err
import com.github.michaelbull.result.unwrap
import electionguard.ballot.EncryptedTally
import electionguard.cli.RunAccumulateTally
import electionguard.core.GroupContext
import electionguard.core.getSystemTimeInMillis
import electionguard.core.productionGroup
import electionguard.publish.makeConsumer
import electionguard.util.ErrorMessages
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull

class RunTallyAccumulationTest {

Expand Down Expand Up @@ -32,4 +42,23 @@ class RunTallyAccumulationTest {
)
)
}

@Test
fun runTallyAccumulationTestJsonNoBallots() {
val group = productionGroup()
val consumerIn = makeConsumer(group, "src/commonTest/data/workflow/someAvailableJson")
val initResult = consumerIn.readElectionInitialized()
val electionInit = initResult.unwrap()
val manifest = consumerIn.makeManifest(electionInit.config.manifestBytes)

val accumulator = AccumulateTally(group, manifest, "name", electionInit.extendedBaseHash, electionInit.jointPublicKey())
// nothing accumulated
val tally: EncryptedTally = accumulator.build()
assertNotNull(tally)
/*
tally.contests.forEach { it.selections.forEach {
assertEquals( it.encryptedVote ) // show its an encryption of zero - only by decrypting it
}}
*/
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ class AttackEncryptedBallotTest {

if (showCount) {
// sum it up
val accumulator = AccumulateTally(group, electionRecord.manifest(), "attackedTally", electionRecord.extendedBaseHash()!!)
val accumulator = AccumulateTally(group, electionRecord.manifest(), "attackedTally",
electionRecord.extendedBaseHash()!!, ElGamalPublicKey(electionRecord.jointPublicKey()!!))
for (encryptedBallot in mungedBallots ) {
accumulator.addCastBallot(encryptedBallot)
accumulator.addCastBallot(encryptedBallot, ErrorMessages(""))
}
val encryptedTally: EncryptedTally = accumulator.build()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class WorkflowEncryptDecryptTest {
val evote3 = vote.encrypt(publicKey, group.randomElementModQ(minimum = 1))

val accum = listOf(evote1, evote2, evote3)
val eAccum = accum.encryptedSum()
val eAccum = accum.encryptedSum()?: 0.encrypt(publicKey)

//decrypt
val partialDecryption = eAccum.computeShare(keypair.secretKey)
Expand Down Expand Up @@ -112,7 +112,7 @@ class WorkflowEncryptDecryptTest {

// tally
val accum = listOf(evote1, evote2, evote3)
val eAccum = accum.encryptedSum()
val eAccum = accum.encryptedSum()?: 0.encrypt(publicKey)

//decrypt
val shares = trustees.map { eAccum.pad powP it.secretKey.key }
Expand Down
35 changes: 25 additions & 10 deletions egklib/src/jvmMain/kotlin/electionguard/cli/RunAccumulateTally.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import electionguard.core.productionGroup
import electionguard.publish.makeConsumer
import electionguard.publish.makePublisher
import electionguard.tally.AccumulateTally
import electionguard.util.ErrorMessages
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.cli.ArgParser
import kotlinx.cli.ArgType
import kotlinx.cli.required
Expand All @@ -23,6 +25,8 @@ import kotlin.math.roundToInt
class RunAccumulateTally {

companion object {
private val logger = KotlinLogging.logger("RunAccumulateTally")

@JvmStatic
fun main(args: Array<String>) {
val parser = ArgParser("RunAccumulateTally")
Expand Down Expand Up @@ -50,13 +54,17 @@ class RunAccumulateTally {
println("RunAccumulateTally starting\n input= $inputDir\n output = $outputDir")

val group = productionGroup()
runAccumulateBallots(
group,
inputDir,
outputDir,
name ?: "RunAccumulateTally",
createdBy ?: "RunAccumulateTally"
)
try {
runAccumulateBallots(
group,
inputDir,
outputDir,
name ?: "RunAccumulateTally",
createdBy ?: "RunAccumulateTally"
)
} catch (t: Throwable) {
logger.error { "Exception= ${t.message} ${t.stackTraceToString()}" }
}
}

fun runAccumulateBallots(
Expand All @@ -79,10 +87,17 @@ class RunAccumulateTally {

var countBad = 0
var countOk = 0
val accumulator = AccumulateTally(group, manifest, name, electionInit.extendedBaseHash)
val accumulator = AccumulateTally(group, manifest, name, electionInit.extendedBaseHash, electionInit.jointPublicKey())
for (encryptedBallot in consumerIn.iterateAllCastBallots()) {
val ok = accumulator.addCastBallot(encryptedBallot)
if (ok) countOk++ else countBad++
val errs = ErrorMessages("RunAccumulateTally ballotId=${encryptedBallot.ballotId}")
accumulator.addCastBallot(encryptedBallot, errs)
if (errs.hasErrors()) {
println(errs)
logger.error{ errs.toString() }
countBad++
} else {
countOk++
}
}
val tally: EncryptedTally = accumulator.build()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ fun SimplePlaintextBallot.encrypt(context: GroupContext, keypair: ElGamalKeypair
val selectionsAndProofs = plaintextWithNonceAndCiphertext.mapIndexed { i, (p, n, c) ->
Pair(c, c.makeChaumPedersen(p, 1, n, keypair.publicKey, proofNonces[i].toUInt256safe()))
}
val encryptedSum = selectionsAndProofs.map { it.first }.encryptedSum()
val encryptedSum = selectionsAndProofs.map { it.first }.encryptedSum()?: 0.encrypt(keypair.publicKey)
val nonceSum = plaintextWithNonce.map { it.second }.reduce { a, b -> a + b }
val plaintextSum = selections.sum()
// fun ElGamalCiphertext.rangeChaumPedersenProofKnownNonce(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import electionguard.encrypt.cast
import electionguard.cli.ManifestBuilder
import electionguard.input.RandomBallotProvider
import electionguard.tally.AccumulateTally
import electionguard.util.ErrorMessages
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
Expand Down Expand Up @@ -64,9 +65,9 @@ class BallotAggregationTestVector {
encryptor.encrypt(ballot, ByteArray(0))
}

val accumulator = AccumulateTally(group, manifest, "makeBallotAggregationTestVector", extendedBaseHash)
val accumulator = AccumulateTally(group, manifest, "makeBallotAggregationTestVector", extendedBaseHash, ElGamalPublicKey(publicKey))
eballots.forEach { eballot ->
accumulator.addCastBallot(eballot.cast())
accumulator.addCastBallot(eballot.cast(), ErrorMessages(""))
}
val tally = accumulator.build()

Expand Down Expand Up @@ -95,11 +96,12 @@ class BallotAggregationTestVector {
}

val extended_base_hash = testVector.extended_base_hash.import() ?: throw IllegalArgumentException("readBallotAggregationTestVector malformed extended_base_hash")
val joint_public_key = testVector.joint_public_key.import(group) ?: throw IllegalArgumentException("readBallotAggregationTestVector malformed joint_public_key")
val eballots: List<EncryptedBallotIF> = testVector.encrypted_ballots.map { it.import(group, extended_base_hash) }
val manifest = EncryptedBallotJsonManifestFacade(testVector.encrypted_ballots[0])

val accumulator = AccumulateTally(group, manifest, "makeBallotAggregationTestVector", extended_base_hash)
eballots.forEach { eballot -> accumulator.addCastBallot(eballot) }
val accumulator = AccumulateTally(group, manifest, "makeBallotAggregationTestVector", extended_base_hash, ElGamalPublicKey(joint_public_key))
eballots.forEach { eballot -> accumulator.addCastBallot(eballot, ErrorMessages("")) }
val tally = accumulator.build()

testVector.expected_encrypted_tally.contests.zip(tally.contests).forEach { (expectContest, actualContest) ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ class BallotEncryptionTestVector {
val randomCj = expectContest.expected_proof.proofs.map { it.c_nonce.import(group) ?: throw IllegalArgumentException("readBallotEncryptionTestVector malformed c_nonce") }

val ciphertexts: List<ElGamalCiphertext> = actualContest.selections.map { it.ciphertext }
val contestAccumulation: ElGamalCiphertext = ciphertexts.encryptedSum()
val contestAccumulation: ElGamalCiphertext = ciphertexts.encryptedSum()?: 0.encrypt(publicKey)
val nonces: Iterable<ElementModQ> = actualContest.selections.map { it.selectionNonce }
val aggNonce: ElementModQ = with(group) { nonces.addQ() }
val totalVotes: Int = plainContest.selections.map { it.vote }.sum()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,9 @@ class TallyDecryptionTestVector(
encryptor.encrypt(ballot, ByteArray(0))
}

val accumulator = AccumulateTally(group, manifest, "makeBallotAggregationTestVector", extendedBaseHash)
val accumulator = AccumulateTally(group, manifest, "makeBallotAggregationTestVector", extendedBaseHash, ElGamalPublicKey(publicKey))
eballots.forEach { eballot ->
accumulator.addCastBallot(eballot.cast())
accumulator.addCastBallot(eballot.cast(), ErrorMessages(""))
}
val encryptedTally = accumulator.build()

Expand Down

0 comments on commit 753d429

Please sign in to comment.