Skip to content

Commit 4a4f17e

Browse files
committed
Async ciphertext APIs for rotation & mod-switching
1 parent 9e4c6c2 commit 4a4f17e

File tree

6 files changed

+204
-64
lines changed

6 files changed

+204
-64
lines changed

Sources/HomomorphicEncryption/Ciphertext.swift

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -937,4 +937,90 @@ extension Ciphertext {
937937
}
938938
throw HeError.errorCastingPolyFormat(from: Format.self, to: Scheme.CanonicalCiphertextFormat.self)
939939
}
940+
941+
// MARK: Async rotations
942+
943+
/// Asynchronously rotates the columns of a ciphertext.
944+
///
945+
/// - Parameters:
946+
/// - step: Number of slots to rotate. Negative values indicate a left rotation, and positive values indicate a
947+
/// right rotation. Must have absolute value in `[1, N / 2 - 1]` where `N` is the RLWE ring dimension, given by
948+
/// ``EncryptionParameters/polyDegree``.
949+
/// - evaluationKey: Evaluation key to use in the HE computation. Must contain the Galois element associated with
950+
/// `step`, see ``GaloisElement/rotatingColumns(by:degree:)``.
951+
/// - Throws: failure to rotate ciphertext's columns.
952+
/// - seealso: ``HeScheme/rotateColumns(of:by:using:)-7h3fz`` for an alternate API and more information.
953+
@inlinable
954+
public mutating func rotateColumns(by step: Int,
955+
using evaluationKey: EvaluationKey<Scheme>) async throws
956+
where Format == Scheme.CanonicalCiphertextFormat
957+
{
958+
try await Scheme.rotateColumnsAsync(of: &self, by: step, using: evaluationKey)
959+
}
960+
961+
/// Asynchronously swaps the rows of a ciphertext.
962+
///
963+
/// A plaintext in ``EncodeFormat/simd`` format can be viewed a `2 x (N / 2)` matrix of coefficients.
964+
/// For instance, for `N = 8`, given a ciphertext encrypting a plaintext with values
965+
/// ```
966+
/// [1, 2, 3, 4, 5, 6, 7, 8]
967+
/// ```
968+
/// calling ``HeScheme/swapRows(of:using:)`` with `step: 1` will yield a ciphertext decrypting to
969+
/// ```
970+
/// [5, 6, 7, 8, 1, 2, 3, 4]
971+
/// ```
972+
/// - Parameter evaluationKey: Evaluation key to use in the HE computation. Must contain the Galois element
973+
/// associated with `step`, see ``GaloisElement/rotatingColumns(by:degree:)``.
974+
/// - Throws: error upon failure to swap the ciphertext's rows.
975+
/// - seealso: ``HeScheme/swapRows(of:using:)-50tac`` for an alternate API.
976+
@inlinable
977+
public mutating func swapRows(using evaluationKey: EvaluationKey<Scheme>) async throws
978+
where Format == Scheme.CanonicalCiphertextFormat
979+
{
980+
try await Scheme.swapRowsAsync(of: &self, using: evaluationKey)
981+
}
982+
983+
/// Asynchronously performs modulus switching on the ciphertext.
984+
///
985+
/// - Throws: Error upon failure to mod-switch.
986+
/// - seealso: ``HeScheme/modSwitchDown(_:)`` for an alternative API and more information.
987+
@inlinable
988+
public mutating func modSwitchDown() async throws where Format == Scheme.CanonicalCiphertextFormat {
989+
try await Scheme.modSwitchDownAsync(&self)
990+
}
991+
992+
/// Asynchronously performs modulus switching to a single modulus.
993+
///
994+
/// If the ciphertext already has a single modulus, this is a no-op.
995+
/// - Throws: Error upon failure to modulus switch.
996+
/// - seealso: ``Ciphertext/modSwitchDown()`` for more information and an alternative API.
997+
@inlinable
998+
public mutating func modSwitchDownToSingle() async throws where Format == Scheme.CanonicalCiphertextFormat {
999+
try await Scheme.modSwitchDownToSingleAsync(&self)
1000+
}
1001+
}
1002+
1003+
extension Ciphertext where Format == Scheme.CanonicalCiphertextFormat {
1004+
/// Asynchronously applies a Galois transformation.
1005+
///
1006+
/// - Parameters:
1007+
/// - element: Galois element of the transformation. Must be odd in `[1, 2 * N - 1]` where `N` is the RLWE ring
1008+
/// dimension, given by ``EncryptionParameters/polyDegree``.
1009+
/// - key: Evaluation key. Must contain Galois element `element`.
1010+
/// - Throws: Error upon failure to apply the Galois transformation.
1011+
/// - seealso: ``HeScheme/applyGalois(ciphertext:element:using:)`` for an alternative API and more information.
1012+
@inlinable
1013+
public mutating func applyGalois(element: Int, using key: EvaluationKey<Scheme>) async throws {
1014+
try await Scheme.applyGaloisAsync(ciphertext: &self, element: element, using: key)
1015+
}
1016+
1017+
/// Asynchronously Relinearizes the ciphertext.
1018+
///
1019+
/// - Parameter key: Evaluation key to relinearize with. Must contain a `RelinearizationKey`.
1020+
/// - Throws: Error upon failure to relinearize.
1021+
/// - seealso: ``HeScheme/relinearize(_:using:)`` for an alternative API and more information.
1022+
@inlinable
1023+
public mutating func relinearize(using key: EvaluationKey<Scheme>) async throws {
1024+
try await Scheme.relinearizeAsync(&self, using: key)
1025+
}
9401026
}

Sources/PrivateNearestNeighborSearch/CiphertextMatrix.swift

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -337,10 +337,7 @@ extension CiphertextMatrix {
337337
let rotateCount = simdColumnCount / (copiesInMask * columnCountPowerOfTwo) - 1
338338
var ciphertextCopyRight = ciphertext
339339
for await _ in (0..<rotateCount).async {
340-
try await Scheme.rotateColumnsAsync(
341-
of: &ciphertextCopyRight,
342-
by: columnCountPowerOfTwo,
343-
using: evaluationKey)
340+
try await ciphertextCopyRight.rotateColumns(by: columnCountPowerOfTwo, using: evaluationKey)
344341
try await ciphertext += ciphertextCopyRight
345342
}
346343
// e.g., `ciphertext` now encrypts
@@ -349,7 +346,7 @@ extension CiphertextMatrix {
349346

350347
// Duplicate values to both SIMD rows
351348
var ciphertextCopy = ciphertext
352-
try await Scheme.swapRowsAsync(of: &ciphertextCopy, using: evaluationKey)
349+
try await ciphertextCopy.swapRows(using: evaluationKey)
353350
try await ciphertext += ciphertextCopy
354351
// e.g., `ciphertext` now encrypts
355352
// [[3, 4, 3, 4, 3, 4, 3, 4],

Sources/PrivateNearestNeighborSearch/MatrixMultiplication.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ extension PlaintextMatrix {
176176
for step in 0..<babyStepGiantStep.babyStep {
177177
rotatedStates.append(state)
178178
if step != babyStepGiantStep.babyStep - 1 {
179-
try await Scheme.rotateColumnsAsync(of: &state, by: -1, using: evaluationKey)
179+
try await state.rotateColumns(by: -1, using: evaluationKey)
180180
}
181181
}
182182
let rotatedCiphertexts: [Scheme.EvalCiphertext] = try await .init(

Sources/_HomomorphicEncryptionExtras/Ciphertext.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,23 @@ extension Ciphertext {
3232
{
3333
try Scheme.rotateColumnsMultiStep(of: &self, by: step, using: evaluationKey)
3434
}
35+
36+
/// Asynchronously rotates the columns of the ciphertext by combining multiple rotation steps corresponding to
37+
/// Galois elements
38+
/// available in the `evaluationKey`.
39+
///
40+
/// - Parameters:
41+
/// - step: Number of slots to rotate. Negative values indicate a left rotation, and positive values indicate a
42+
/// right rotation. Must have absolute value in `[1, N / 2 - 1]` where `N` is the RLWE ring dimension, given by
43+
/// `EncryptionParameters/polyDegree`.
44+
/// - evaluationKey: Evaluation key to use in the HE computation. Must contain Galois elements which can be
45+
/// combined for the desired rotation step.
46+
/// - Throws: Error upon failure to rotate ciphertext's columns.
47+
/// - seealso: `HeScheme/_rotateColumnsMultiStep(of:by:using:)` for an alternative API and more information.
48+
@inlinable
49+
public mutating func rotateColumnsMultiStep(by step: Int, using evaluationKey: EvaluationKey<Scheme>) async throws
50+
where Format == Scheme.CanonicalCiphertextFormat
51+
{
52+
try await Scheme.rotateColumnsMultiStepAsync(of: &self, by: step, using: evaluationKey)
53+
}
3554
}

Sources/_TestUtilities/HeApiTestUtils.swift

Lines changed: 94 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,7 @@ public enum HeAPITestHelpers {
567567
try await ciphertextProduct2 *= ciphertext2
568568

569569
var relinearizedProd = ciphertextProduct
570-
try relinearizedProd.relinearize(using: #require(testEnv.evaluationKey))
570+
try await relinearizedProd.relinearize(using: #require(testEnv.evaluationKey))
571571
#expect(relinearizedProd.polys.count == Scheme.freshCiphertextPolyCount)
572572

573573
let coeffCiphertext = try await ciphertextProduct.convertToCoeffFormat()
@@ -1233,7 +1233,7 @@ public enum HeAPITestHelpers {
12331233
// with mod-switch down
12341234
if context.coefficientModuli.count > 2 {
12351235
var ciphertext = testEnv.ciphertext1
1236-
try ciphertext.modSwitchDown()
1236+
try await ciphertext.modSwitchDown()
12371237
let evalCiphertext = try await ciphertext.convertToEvalFormat()
12381238
let moduliCount = evalCiphertext.moduli.count
12391239
let evalPlaintext = try testEnv.context.encode(values: data2, format: .simd, moduliCount: moduliCount)
@@ -1253,84 +1253,108 @@ public enum HeAPITestHelpers {
12531253
context: Scheme.Context,
12541254
scheme _: Scheme.Type) async throws
12551255
{
1256-
func runRotationTest(context: Scheme.Context, galoisElements: [Int], multiStep: Bool) async throws {
1257-
let degree = context.degree
1256+
func runRotationTestSync(context: Scheme.Context, galoisElements: [Int], multiStep: Bool) throws {
12581257
let testEnv = try TestEnv<Scheme>(context: context, format: .simd, galoisElements: galoisElements)
12591258
let evaluationKey = try #require(testEnv.evaluationKey)
1259+
let degree = context.degree
1260+
12601261
for step in 1..<min(8, degree / 2) {
12611262
let expectedData = Array(testEnv.data1[degree / 2 - step..<degree / 2] + testEnv
12621263
.data1[0..<degree / 2 - step] + testEnv
12631264
.data1[degree - step..<degree] + testEnv.data1[degree / 2..<degree - step])
12641265
var rotatedCiphertext = testEnv.ciphertext1
1265-
var rotatedCiphertextAsync = testEnv.ciphertext1
12661266
if multiStep {
12671267
try rotatedCiphertext.rotateColumnsMultiStep(by: step, using: evaluationKey)
1268-
try await Scheme.rotateColumnsMultiStepAsync(
1269-
of: &rotatedCiphertextAsync,
1270-
by: step,
1271-
using: evaluationKey)
12721268
} else {
12731269
try rotatedCiphertext.rotateColumns(by: step, using: evaluationKey)
1274-
try await Scheme.rotateColumnsAsync(of: &rotatedCiphertextAsync, by: step, using: evaluationKey)
12751270
}
12761271
try testEnv.checkDecryptsDecodes(ciphertext: rotatedCiphertext, format: .simd, expected: expectedData)
1277-
try testEnv.checkDecryptsDecodes(
1278-
ciphertext: rotatedCiphertextAsync,
1279-
format: .simd,
1280-
expected: expectedData)
12811272

12821273
if multiStep {
12831274
try rotatedCiphertext.rotateColumnsMultiStep(by: -step, using: evaluationKey)
1284-
try await Scheme.rotateColumnsMultiStepAsync(
1285-
of: &rotatedCiphertextAsync,
1286-
by: -step,
1287-
using: evaluationKey)
12881275
} else {
12891276
try rotatedCiphertext.rotateColumns(by: -step, using: evaluationKey)
1290-
try await Scheme.rotateColumnsAsync(of: &rotatedCiphertextAsync, by: -step, using: evaluationKey)
12911277
}
1292-
try testEnv.checkDecryptsDecodes(ciphertext: rotatedCiphertext,
1293-
format: .simd,
1294-
expected: testEnv.data1)
1295-
try testEnv.checkDecryptsDecodes(ciphertext: rotatedCiphertextAsync,
1296-
format: .simd,
1297-
expected: testEnv.data1)
1278+
try testEnv.checkDecryptsDecodes(ciphertext: rotatedCiphertext, format: .simd, expected: testEnv.data1)
1279+
}
1280+
}
1281+
1282+
func runRotationTestAsync(context: Scheme.Context, galoisElements: [Int], multiStep: Bool) async throws {
1283+
let testEnv = try TestEnv<Scheme>(context: context, format: .simd, galoisElements: galoisElements)
1284+
let evaluationKey = try #require(testEnv.evaluationKey)
1285+
let degree = context.degree
1286+
1287+
for step in 1..<min(8, degree / 2) {
1288+
let expectedData = Array(testEnv.data1[degree / 2 - step..<degree / 2] + testEnv
1289+
.data1[0..<degree / 2 - step] + testEnv
1290+
.data1[degree - step..<degree] + testEnv.data1[degree / 2..<degree - step])
1291+
var rotatedCiphertext = testEnv.ciphertext1
1292+
if multiStep {
1293+
try await rotatedCiphertext.rotateColumnsMultiStep(by: step, using: evaluationKey)
1294+
} else {
1295+
try await rotatedCiphertext.rotateColumns(by: step, using: evaluationKey)
1296+
}
1297+
try testEnv.checkDecryptsDecodes(ciphertext: rotatedCiphertext, format: .simd, expected: expectedData)
1298+
1299+
if multiStep {
1300+
try await rotatedCiphertext.rotateColumnsMultiStep(by: -step, using: evaluationKey)
1301+
} else {
1302+
try await rotatedCiphertext.rotateColumns(by: -step, using: evaluationKey)
1303+
}
1304+
try testEnv.checkDecryptsDecodes(ciphertext: rotatedCiphertext, format: .simd, expected: testEnv.data1)
12981305
}
12991306
}
13001307

13011308
guard context.supportsSimdEncoding, context.supportsEvaluationKey else {
13021309
return
13031310
}
13041311

1312+
let degree = context.degree
1313+
let galoisElementsRotate = try (1..<(degree >> 1)).flatMap { step in
1314+
try [
1315+
GaloisElement.rotatingColumns(by: step, degree: degree),
1316+
GaloisElement.rotatingColumns(by: -step, degree: degree),
1317+
]
1318+
}
1319+
let galoisElementsMultiStep = try GaloisElement.rotatingColumnsMultiStep(degree: degree)
1320+
1321+
try runRotationTestSync(context: context, galoisElements: galoisElementsRotate, multiStep: false)
1322+
try await runRotationTestAsync(context: context, galoisElements: galoisElementsMultiStep, multiStep: true)
1323+
}
1324+
1325+
/// Testing ciphertext rotation of the scheme.
1326+
@inlinable
1327+
public static func schemeSwapRowsTest<Scheme: HeScheme>(
1328+
context: Scheme.Context,
1329+
scheme _: Scheme.Type) async throws
1330+
{
1331+
guard context.supportsSimdEncoding, context.supportsEvaluationKey else {
1332+
return
1333+
}
13051334
let degree = context.degree
13061335
let galoisElementsSwap = [GaloisElement.swappingRows(degree: degree)]
13071336
let testEnv = try TestEnv<Scheme>(context: context, format: .simd, galoisElements: galoisElementsSwap)
13081337
let evaluationKey = try #require(testEnv.evaluationKey)
13091338
let expectedData = Array(testEnv.data1[degree / 2..<degree] + testEnv.data1[0..<degree / 2])
13101339
var ciphertext = testEnv.ciphertext1
1311-
var ciphertextAsync = ciphertext
13121340

1313-
try ciphertext.swapRows(using: evaluationKey)
1314-
try await Scheme.swapRowsAsync(of: &ciphertextAsync, using: evaluationKey)
1341+
func swapRowsSyncTest() throws {
1342+
try ciphertext.swapRows(using: evaluationKey)
1343+
try testEnv.checkDecryptsDecodes(ciphertext: ciphertext, format: .simd, expected: expectedData)
13151344

1316-
try testEnv.checkDecryptsDecodes(ciphertext: ciphertext, format: .simd, expected: expectedData)
1317-
try testEnv.checkDecryptsDecodes(ciphertext: ciphertextAsync, format: .simd, expected: expectedData)
1345+
try ciphertext.swapRows(using: evaluationKey)
1346+
try testEnv.checkDecryptsDecodes(ciphertext: ciphertext, format: .simd, expected: testEnv.data1)
1347+
}
1348+
try swapRowsSyncTest()
13181349

1319-
try ciphertext.swapRows(using: evaluationKey)
1320-
try await Scheme.swapRowsAsync(of: &ciphertextAsync, using: evaluationKey)
1321-
try testEnv.checkDecryptsDecodes(ciphertext: ciphertext, format: .simd, expected: testEnv.data1)
1322-
try testEnv.checkDecryptsDecodes(ciphertext: ciphertextAsync, format: .simd, expected: testEnv.data1)
1350+
func swapRowsAsyncTest() async throws {
1351+
try await ciphertext.swapRows(using: evaluationKey)
1352+
try testEnv.checkDecryptsDecodes(ciphertext: ciphertext, format: .simd, expected: expectedData)
13231353

1324-
let galoisElementsRotate = try (1..<(degree >> 1)).flatMap { step in
1325-
try [
1326-
GaloisElement.rotatingColumns(by: step, degree: degree),
1327-
GaloisElement.rotatingColumns(by: -step, degree: degree),
1328-
]
1354+
try await ciphertext.swapRows(using: evaluationKey)
1355+
try testEnv.checkDecryptsDecodes(ciphertext: ciphertext, format: .simd, expected: testEnv.data1)
13291356
}
1330-
let galoisElementsMultiStep = try GaloisElement.rotatingColumnsMultiStep(degree: degree)
1331-
1332-
try await runRotationTest(context: context, galoisElements: galoisElementsRotate, multiStep: false)
1333-
try await runRotationTest(context: context, galoisElements: galoisElementsMultiStep, multiStep: true)
1357+
try await swapRowsAsyncTest()
13341358
}
13351359

13361360
/// Testing apply Galois element of the scheme.
@@ -1358,25 +1382,37 @@ public enum HeAPITestHelpers {
13581382

13591383
let dataCount = testEnv.data1.count
13601384
let halfDataCount = dataCount / 2
1361-
for (step, element) in elements.enumerated() {
1362-
for modSwitchCount in 0...max(0, context.coefficientModuli.count - 2) {
1363-
var rotatedCiphertext = testEnv.ciphertext1
1364-
var rotatedCiphertextAsync = testEnv.ciphertext1
1385+
func syncTest() throws {
1386+
for (step, element) in elements.enumerated() {
1387+
for modSwitchCount in 0...max(0, context.coefficientModuli.count - 2) {
1388+
var rotatedCiphertext = testEnv.ciphertext1
1389+
1390+
for _ in 0..<modSwitchCount {
1391+
try rotatedCiphertext.modSwitchDown()
1392+
}
1393+
try rotatedCiphertext.applyGalois(element: element, using: evaluationKey)
1394+
let expected = rotate(original: testEnv.data1, halfDataCount: halfDataCount, step: step + 1)
1395+
try testEnv.checkDecryptsDecodes(ciphertext: rotatedCiphertext, format: .simd, expected: expected)
1396+
}
1397+
}
1398+
}
1399+
try syncTest()
13651400

1366-
for _ in 0..<modSwitchCount {
1367-
try rotatedCiphertext.modSwitchDown()
1368-
try await Scheme.modSwitchDownAsync(&rotatedCiphertextAsync)
1401+
func asyncTest() async throws {
1402+
for (step, element) in elements.enumerated() {
1403+
for modSwitchCount in 0...max(0, context.coefficientModuli.count - 2) {
1404+
var rotatedCiphertext = testEnv.ciphertext1
1405+
1406+
for _ in 0..<modSwitchCount {
1407+
try await rotatedCiphertext.modSwitchDown()
1408+
}
1409+
try await rotatedCiphertext.applyGalois(element: element, using: evaluationKey)
1410+
let expected = rotate(original: testEnv.data1, halfDataCount: halfDataCount, step: step + 1)
1411+
try testEnv.checkDecryptsDecodes(ciphertext: rotatedCiphertext, format: .simd, expected: expected)
13691412
}
1370-
try rotatedCiphertext.applyGalois(element: element, using: evaluationKey)
1371-
try await Scheme.applyGaloisAsync(
1372-
ciphertext: &rotatedCiphertextAsync,
1373-
element: element,
1374-
using: evaluationKey)
1375-
let expected = rotate(original: testEnv.data1, halfDataCount: halfDataCount, step: step + 1)
1376-
try testEnv.checkDecryptsDecodes(ciphertext: rotatedCiphertext, format: .simd, expected: expected)
1377-
try testEnv.checkDecryptsDecodes(ciphertext: rotatedCiphertextAsync, format: .simd, expected: expected)
13781413
}
13791414
}
1415+
try await asyncTest()
13801416
}
13811417

13821418
/// Testing noise budget estimation.

0 commit comments

Comments
 (0)