Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AccumulateTally uses ErrorMessages (no Exceptions). #423

Merged
merged 1 commit into from
Nov 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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