Skip to content

Commit

Permalink
Merge pull request #452 from JohnLCaron/encryptTiming
Browse files Browse the repository at this point in the history
Add cli to run encryption timing.
  • Loading branch information
JohnLCaron authored Feb 6, 2024
2 parents 4b9469b + 5240ace commit 41cf766
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,57 @@ fun List<ElGamalCiphertext>.add(other: List<ElGamalCiphertext>): List<ElGamalCip
result.add(value + other[index])
}
return result
}

fun Int.encrypt(
keypair: ElGamalKeypair,
nonce: ElementModQ = keypair.context.randomElementModQ(minimum = 1)
) = this.encrypt(keypair.publicKey, nonce)

/** Encrypt an Int. */
fun Int.encrypt(
publicKey: ElGamalPublicKey,
nonce: ElementModQ = publicKey.context.randomElementModQ(minimum = 1)
): ElGamalCiphertext {
val context = compatibleContextOrFail(publicKey.key, nonce)

// LOOK: Exception
if (nonce.isZero()) {
throw ArithmeticException("Can't use a zero nonce for ElGamal encryption")
}

if (this < 0) {
throw ArithmeticException("Can't encrypt a negative vote")
}

// We don't have to check if message >= Q, because it's an integer, and Q is much larger than that.
// Enc(σ, ξ) = (α, β) = (g^ξ mod p, K^σ · K^ξ mod p) = (g^ξ mod p, K^(σ+ξ) mod p). spec 2.0.0 eq 24
val pad = context.gPowP(nonce)
val data = publicKey.key powP (nonce + this.toElementModQ(context))

return ElGamalCiphertext(pad, data)
}

/** Encrypt a Long. Used to encrypt serial number. */
fun Long.encrypt(
publicKey: ElGamalPublicKey,
nonce: ElementModQ = publicKey.context.randomElementModQ(minimum = 1)
): ElGamalCiphertext {
val context = compatibleContextOrFail(publicKey.key, nonce)

// LOOK: Exception
if (nonce.isZero()) {
throw ArithmeticException("Can't use a zero nonce for ElGamal encryption")
}

if (this < 0) {
throw ArithmeticException("Can't encrypt a negative value")
}

// We don't have to check if message >= Q, because it's a long, and Q is much larger than that.
// Enc(σ, ξ) = (α, β) = (g^ξ mod p, K^σ · K^ξ mod p) = (g^ξ mod p, K^(σ+ξ) mod p). spec 2.0.0 eq 24
val pad = context.gPowP(nonce)
val data = publicKey.key powP (nonce + this.toElementModQ(context))

return ElGamalCiphertext(pad, data)
}
57 changes: 1 addition & 56 deletions egklib/src/commonMain/kotlin/electionguard/core/ElGamalKeys.kt
Original file line number Diff line number Diff line change
Expand Up @@ -90,59 +90,4 @@ fun elGamalKeyPairFromSecret(secret: ElementModQ) =

/** Generates a random ElGamal keypair. */
fun elGamalKeyPairFromRandom(context: GroupContext) =
elGamalKeyPairFromSecret(context.randomElementModQ(minimum = 2))

/** Uses an ElGamal public key to encrypt an unsigned integer vote. An optional nonce can be specified to make this
* deterministic, or it will be chosen at random. see eq 24. */
fun Int.encrypt(
publicKey: ElGamalPublicKey,
nonce: ElementModQ = publicKey.context.randomElementModQ(minimum = 1)
): ElGamalCiphertext {
val context = compatibleContextOrFail(publicKey.key, nonce)

// LOOK: Exception
if (nonce.isZero()) {
throw ArithmeticException("Can't use a zero nonce for ElGamal encryption")
}

if (this < 0) {
throw ArithmeticException("Can't encrypt a negative vote")
}

// We don't have to check if message >= Q, because it's an integer, and Q is much larger than that.
// Enc(σ, ξ) = (α, β) = (g^ξ mod p, K^σ · K^ξ mod p) = (g^ξ mod p, K^(σ+ξ) mod p). spec 2.0.0 eq 24
val pad = context.gPowP(nonce)
val data = publicKey.key powP (nonce + this.toElementModQ(context))

return ElGamalCiphertext(pad, data)
}

/** Uses an ElGamal public key to encrypt an integer vote. An optional nonce can be specified to make this
* deterministic, or it will be chosen at random. see eq 24. */
fun Long.encrypt(
publicKey: ElGamalPublicKey,
nonce: ElementModQ = publicKey.context.randomElementModQ(minimum = 1)
): ElGamalCiphertext {
val context = compatibleContextOrFail(publicKey.key, nonce)

// LOOK: Exception
if (nonce.isZero()) {
throw ArithmeticException("Can't use a zero nonce for ElGamal encryption")
}

if (this < 0) {
throw ArithmeticException("Can't encrypt a negative value")
}

// We don't have to check if message >= Q, because it's a long, and Q is much larger than that.
// Enc(σ, ξ) = (α, β) = (g^ξ mod p, K^σ · K^ξ mod p) = (g^ξ mod p, K^(σ+ξ) mod p). spec 2.0.0 eq 24
val pad = context.gPowP(nonce)
val data = publicKey.key powP (nonce + this.toElementModQ(context))

return ElGamalCiphertext(pad, data)
}

fun Int.encrypt(
keypair: ElGamalKeypair,
nonce: ElementModQ = keypair.context.randomElementModQ(minimum = 1)
) = this.encrypt(keypair.publicKey, nonce)
elGamalKeyPairFromSecret(context.randomElementModQ(minimum = 2))
147 changes: 147 additions & 0 deletions egklib/src/commonTest/kotlin/electionguard/core/PowRadixTiming.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package electionguard.core

import electionguard.util.Stopwatch
import electionguard.util.pad
import electionguard.util.sigfig
import kotlin.test.Test

class PowRadixTiming {

@Test
fun timeLow() {
println("PowRadixOption.LOW_MEMORY_USE")
val groupLow = productionGroup(PowRadixOption.LOW_MEMORY_USE, ProductionMode.Mode4096 )
val nonce = groupLow.randomElementModQ()
groupLow.gPowP(nonce) // first time fills table
testWarmup(groupLow)
}
// PowRadixOption.LOW_MEMORY_USE
// 0 acc 1000 = .851 msec per acc
//1000 acc 1000 = .848 msec per acc
//2000 acc 1000 = .852 msec per acc
//3000 acc 1000 = .759 msec per acc // HotSpot warmup
//4000 acc 1000 = .757 msec per acc
//5000 acc 1000 = .752 msec per acc
//6000 acc 1000 = .749 msec per acc
//7000 acc 1000 = .755 msec per acc
//8000 acc 1000 = .755 msec per acc
//9000 acc 1000 = .752 msec per acc

@Test
fun timeHigh() {
println("PowRadixOption.HIGH_MEMORY_USE")
val groupHigh = productionGroup(PowRadixOption.HIGH_MEMORY_USE, ProductionMode.Mode4096 )
val nonce = groupHigh.randomElementModQ()
groupHigh.gPowP(nonce) // first time fills table
testWarmup(groupHigh)
}
// PowRadixOption.HIGH_MEMORY_USE
// 0 acc 1000 = .670 msec per acc
//1000 acc 1000 = .561 msec per acc
//2000 acc 1000 = .615 msec per acc
//3000 acc 1000 = .588 msec per acc
//4000 acc 1000 = .559 msec per acc
//5000 acc 1000 = .573 msec per acc
//6000 acc 1000 = .553 msec per acc
//7000 acc 1000 = .554 msec per acc
//8000 acc 1000 = .536 msec per acc
//9000 acc 1000 = .540 msec per acc

@Test
fun timeExtreme() {
println("PowRadixOption.EXTREME_MEMORY_USE")
val groupExtreme = productionGroup(PowRadixOption.EXTREME_MEMORY_USE, ProductionMode.Mode4096 )
val nonce = groupExtreme.randomElementModQ()
groupExtreme.gPowP(nonce) // first time fills table
testWarmup(groupExtreme)
}
// PowRadixOption.EXTREME_MEMORY_USE
// 0 acc 1000 = .463 msec per acc
//1000 acc 1000 = .421 msec per acc
//2000 acc 1000 = .415 msec per acc
//3000 acc 1000 = .427 msec per acc
//4000 acc 1000 = .422 msec per acc
//5000 acc 1000 = .492 msec per acc
//6000 acc 1000 = .406 msec per acc
//7000 acc 1000 = .404 msec per acc
//8000 acc 1000 = .402 msec per acc
//9000 acc 1000 = .480 msec per acc

fun testWarmup(group: GroupContext) {
var count = 0
val incr = 1000
repeat(10) {
timeAcc(group, count, incr)
count += incr
}
}

fun timeAcc(group: GroupContext, count: Int, n:Int) {
val nonces = List(n) { group.randomElementModQ() }

var stopwatch = Stopwatch()
repeat(n) { require( !group.gPowP(nonces[it]).isZero()) }
var duration = stopwatch.stop()
val peracc = duration.toDouble() / n / 1_000_000
println(" ${count.pad(4)} acc $n = ${peracc.sigfig(3)} msec per acc")
}

@Test
// compare exp vs acc
fun timeExp() {
println("compare exp vs acc")
val group = productionGroup()
compareExp(group,100)
compareExp(group,1000)
compareExp(group,10000)
compareExp(group,20000)
}

fun compareExp(group: GroupContext, n:Int) {
val nonces = List(n) { group.randomElementModQ() }
val h = group.gPowP(group.randomElementModQ())

var stopwatch = Stopwatch()
repeat(n) { require( !group.gPowP(nonces[it]).isZero()) }

var duration = stopwatch.stop()
val peracc = duration.toDouble() / n / 1_000_000
println(" acc took $duration msec for $n = $peracc msec per acc")

stopwatch.start()
repeat(n) { require(!(h powP nonces[it]).isZero()) }

duration = stopwatch.stop()
val perexp = duration.toDouble() / n / 1_000_000
println(" exp took $duration msec for $n = $perexp msec per exp")

println(" exp/acc took ${perexp/peracc}")
}


@Test
fun timeMultiply() {
println("compare exp vs acc")
val group = productionGroup()
timeMultiply(group,1000)
timeMultiply(group,10000)
timeMultiply(group,20000)
}

fun timeMultiply(group: GroupContext, n:Int) {
val nonces = List(n) { group.randomElementModQ() }
val elemps = nonces.map { group.gPowP(it) }

var starting = getSystemTimeInMillis()
val prod = elemps.reduce { a, b -> a * b }
var duration = getSystemTimeInMillis() - starting
var peracc = duration.toDouble() / n
println(" multiply took $duration msec for $n = $peracc msec per multiply")

starting = getSystemTimeInMillis()
elemps.forEach { it * it }
duration = getSystemTimeInMillis() - starting
peracc = duration.toDouble() / n
println(" square took $duration msec for $n = $peracc msec per multiply")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package electionguard.encrypt

import electionguard.cli.RunEncryptBallotTiming
import kotlin.test.Test

class
RunEncryptBallotTimingTest {

@Test
fun testRunEncryptBallotTiming() {
RunEncryptBallotTiming.main(
arrayOf(
)
)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package electionguard.cli

import electionguard.core.*
import electionguard.encrypt.Encryptor
import electionguard.input.RandomBallotProvider
import electionguard.util.ErrorMessages
import electionguard.util.Stopwatch
import electionguard.util.sigfig
import kotlinx.cli.ArgParser
import kotlinx.cli.ArgType
import kotlinx.cli.default

class RunEncryptBallotTiming {

companion object {
@JvmStatic
fun main(args: Array<String>) {
val parser = ArgParser("RunEncryptBallotTiming")
val nballots by parser.option(
ArgType.Int,
shortName = "nballots",
description = "Number of ballots to encrypt and measure time"
).default(100)
val ncontests by parser.option(
ArgType.Int,
shortName = "ncontests",
description = "Number of contests per ballot"
).default(12)
val nselections by parser.option(
ArgType.Int,
shortName = "nselections",
description = "Number of selections per contest"
).default(4)
val warmup by parser.option(
ArgType.Int,
shortName = "warmup",
description = "Number of ballots to encrypt as warmup (not timed)"
).default(11)
val showOperations by parser.option(
ArgType.Boolean,
shortName = "ops",
description = "Show operation count"
).default(false)
parser.parse(args)

println(
"RunEncryptBallotTiming\n" +
" nballots = '$nballots'\n" +
" ncontests = '$ncontests'\n" +
" nselections = '$nselections'\n" +
" warmup = '$warmup'\n"
)

val group = productionGroup()
val manifest = buildTestManifest(ncontests, nselections)
val keypair = elGamalKeyPairFromRandom(group)
val encryptor = Encryptor(group, manifest, keypair.publicKey, UInt256.random(), "device")

// warmup
println("warming up with $warmup ballots")
val warmupProvider = RandomBallotProvider(manifest, warmup)
warmupProvider.ballots().forEach { ballot ->
val encryptedBallot = encryptor.encrypt(ballot, ByteArray(0), ErrorMessages("testEncryption"))
requireNotNull(encryptedBallot)
}

// time it
val ballotProvider = RandomBallotProvider(manifest, nballots)
var stopwatch = Stopwatch()
group.getAndClearOpCounts()
ballotProvider.ballots().forEach { ballot ->
val encryptedBallot = encryptor.encrypt(ballot, ByteArray(0), ErrorMessages("testEncryption"))
requireNotNull(encryptedBallot)
}
val opCounts = group.getAndClearOpCounts()
var duration = stopwatch.stop()

val perballot = duration.toDouble() / nballots / 1_000_000
val nencryptions = ncontests + ncontests * nselections
val perencryption = perballot / nencryptions
println("Encryption took ${duration / 1_000_000_000 } secs for $nballots ballots")
println(" ${perballot.sigfig(3)} msec per ballot")
println(" ${perencryption.sigfig(3)} msec per encryption ($nencryptions encryptions/ballot)")
println()
if (showOperations) {
println("operations:")
println(buildString {
opCounts.forEach { key, value -> println(" $key = $value") }
})
println("expect: ${6 * nencryptions * nballots + 2 * nballots}")
}
}
}
}
1 change: 1 addition & 0 deletions egklib/src/jvmMain/kotlin/electionguard/util/StopWatch.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Stopwatch(running: Boolean = true) {
return this
}

// return elapsed nanoseconds
fun stop(): Long {
val tick: Long = System.nanoTime()
isRunning = false
Expand Down

0 comments on commit 41cf766

Please sign in to comment.