Skip to content

Commit

Permalink
test: add key derivation benchmark tests and docs (#563)
Browse files Browse the repository at this point in the history
* tests: benchamrk for key derivation

* tests: vault time tests

* tests: extract common test template

* tests: fix stats log

* docs: add document derivation results

* docs: fix typo

* docs: add jdk version
  • Loading branch information
patlo-iog authored Jun 22, 2023
1 parent 3d7696b commit d6ff373
Show file tree
Hide file tree
Showing 2 changed files with 204 additions and 0 deletions.
72 changes: 72 additions & 0 deletions docs/general/key-derivation-benchmark.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Key derivation benchmark

This document provides a performance benchmark of a key derivation used inside the PRISM agent
in comparison with a key retrieval from HashiCorp Vault. It should provide a baseline for
future decisions in managing the key material on PRISM agent.

## Test setup

### Environment

__System information__
- Platform: Linux (6.3.7)
- CPU: AMD Ryzen 7 PRO 6850U
- Memory: 30856MiB
- JDK version: OpenJDK 11.0.18
- SBT version: 1.8.0

__JVM options__
- Xmx:4G

The tests can be run by running the `io.iohk.atala.agent.walletapi.benchmark.KeyDerivation`.
The tests are being ignored to avoid running them on CI. When running locally,
the ignore aspect should be removed and the test can be run by

```bash
sbt prismAgentWalletAPI/'testOnly -- -tag benchmark'
```

## Scenario

### Key Derivation

__Setup__

1. Warm-up JVM by running key derivation for 10k iterations
2. Running key derivation for 50k iterations with `N` parallelism
3. Measure the average, maximum and percentile of execution time (p50, p90, p99).
The measurements consider derivation execution time of a single key.

__Results__

Duration in *microseconds*. Lower is better.

| Parallelism | Avg | P50 | P90 | P99 | Max |
|-------------|---------|--------|---------|----------|----------|
| 1 | 406.97 | 402.91 | 418.56 | 437.83 | 4550.13 |
| 8 | 499.59 | 475.20 | 544.83 | 826.22 | 2928.68 |
| 16 | 877.71 | 831.53 | 931.34 | 2278.10 | 10306.08 |
| 32 | 1772.57 | 821.06 | 1327.90 | 20331.60 | 61460.31 |

### Key retrieval from Vault

__Setup__

1. Warm-up JVM, Vault and their connections by setting/getting 100 keys
2. Running querying the KV from Vault for 50k iterations with `N` parallelism
3. Measure the average, maximum and percentile of execution duration (p50, p90, p99).
The measurements consider the query time and serialization time of a single key.

Note: Vault server runs in a docker container on the same machine using in-memory storage.
So the setup may yield optimistic results.

__Results__

Duration in *microseconds*. Lower is better.

| Parallelism | Avg | P50 | P90 | P99 | Max |
|-------------|---------|---------|---------|---------|----------|
| 1 | 575.62 | 535.61 | 703.44 | 798.64 | 11388.56 |
| 8 | 717.67 | 666.64 | 913.04 | 1424.77 | 10854.26 |
| 16 | 1193.11 | 1098.47 | 1690.94 | 2818.88 | 11410.41 |
| 32 | 2379.26 | 2146.38 | 3954.72 | 6376.56 | 22210.13 |
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package io.iohk.atala.agent.walletapi.benchmark

import zio.*
import zio.test.*
import zio.test.Assertion.*
import io.iohk.atala.agent.walletapi.crypto.Apollo
import io.iohk.atala.castor.core.model.did.EllipticCurve
import io.iohk.atala.shared.models.HexString
import io.iohk.atala.agent.walletapi.crypto.DerivationPath
import io.iohk.atala.agent.walletapi.vault.VaultKVClientImpl
import io.iohk.atala.agent.walletapi.vault.VaultKVClient
import io.iohk.atala.test.container.VaultTestContainerSupport
import io.iohk.atala.shared.models.Base64UrlString

object KeyDerivation extends ZIOSpecDefault, VaultTestContainerSupport {

private val seedHex = "00" * 64
private val seed = HexString.fromStringUnsafe(seedHex).toByteArray

override def spec = suite("Key derivation benchamrk")(
deriveKeyBenchmark.provide(Apollo.prism14Layer),
queryKeyBenchmark.provide(vaultKvClientLayer, Apollo.prism14Layer)
) @@ TestAspect.sequential @@ TestAspect.timed @@ TestAspect.tag("benchmark") @@ TestAspect.ignore

private val deriveKeyBenchmark = suite("Key derivation benchmark")(
benchamrkKeyDerivation(1),
benchamrkKeyDerivation(8),
benchamrkKeyDerivation(16),
benchamrkKeyDerivation(32),
) @@ TestAspect.before(deriveKeyWarmUp())

private val queryKeyBenchmark = suite("Query key benchmark - vault storage")(
benchmarkVaultQuery(1),
benchmarkVaultQuery(8),
benchmarkVaultQuery(16),
benchmarkVaultQuery(32),
) @@ TestAspect.before(vaultWarmUp())

private def benchamrkKeyDerivation(parallelism: Int) = {
test(s"derive 50000 keys - $parallelism parallelism") {
for {
apollo <- ZIO.service[Apollo]
durationList <- ZIO
.foreachPar(1 to 50_000) { i =>
Live.live {
apollo.ecKeyFactory
.deriveKeyPair(EllipticCurve.SECP256K1, seed)(derivationPath(keyIndex = i): _*)
.timed
.map(_._1)
}
}
.withParallelism(parallelism)
_ <- logStats(durationList)
} yield assertCompletes
}
}

private def benchmarkVaultQuery(parallelism: Int) = {
test(s"query 50000 keys - $parallelism parallelism") {
for {
vaultClient <- ZIO.service[VaultKVClient]
apollo <- ZIO.service[Apollo]
keyPair <- apollo.ecKeyFactory.generateKeyPair(EllipticCurve.SECP256K1)
encodedKey = Base64UrlString.fromByteArray(keyPair.privateKey.encode).toString()
_ <- ZIO
.foreach(1 to 50_000) { i => vaultClient.set(s"secret/did/prism/key-$i", Map("value" -> encodedKey)) }
durationList <- ZIO
.foreachPar(1 to 50_000) { i =>
Live.live {
vaultClient
.get(s"secret/did/prism/key-$i")
.flatMap { encodedKey =>
val encodedBytes =
Base64UrlString.fromString(encodedKey.get.get("value").get).toOption.get.toByteArray
ZIO.fromTry(apollo.ecKeyFactory.privateKeyFromEncoded(EllipticCurve.SECP256K1, encodedBytes))
}
.timed
.map(_._1)
}
}
.withParallelism(parallelism)
_ <- logStats(durationList)
} yield assertCompletes
}
}

private def deriveKeyWarmUp(n: Int = 10000) = {
for {
_ <- ZIO.debug("running key derivation warm-up")
apollo <- ZIO.service[Apollo]
_ <- ZIO
.foreach(1 to n) { i =>
apollo.ecKeyFactory
.deriveKeyPair(EllipticCurve.SECP256K1, seed)(derivationPath(keyIndex = i): _*)
}
} yield ()
}

private def vaultWarmUp(n: Int = 100) = {
for {
vaultClient <- ZIO.service[VaultKVClient]
_ <- ZIO.debug("running vault warm-up")
_ <- ZIO.foreach(1 to n) { i =>
vaultClient.set(s"secret/warm-up/key-$i", Map("hello" -> "world"))
}
_ <- ZIO.foreach(1 to n) { i =>
vaultClient.get(s"secret/warm-up/key-$i")
}
} yield ()
}

private def derivationPath(keyIndex: Int = 0): Seq[DerivationPath] = {
Seq(
DerivationPath.Hardened(0x1d),
DerivationPath.Hardened(0),
DerivationPath.Hardened(0),
DerivationPath.Hardened(keyIndex),
)
}

private def logStats(durationList: Seq[Duration]) = {
val n = durationList.length
val sortedDurationInMicro = durationList.sorted.map(_.toNanos() / 1000.0)
val avg = sortedDurationInMicro.sum / n
val p50 = sortedDurationInMicro.apply((0.50 * n).toInt)
val p75 = sortedDurationInMicro.apply((0.75 * n).toInt)
val p90 = sortedDurationInMicro.apply((0.90 * n).toInt)
val p99 = sortedDurationInMicro.apply((0.99 * n).toInt)
val max = sortedDurationInMicro.last
ZIO.debug(s"execution time in us. avg: $avg | p50: $p50 | p90: $p90 | p99: $p99 | max: $max")
}
}

0 comments on commit d6ff373

Please sign in to comment.