diff --git a/Package.resolved b/Package.resolved index fab18367..15af7c0b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "3b554910bbc76ac563097effe5ba38eb7a8f36f6260a82637bbbb208737f4735", + "originHash" : "3b1d2ae9a48864e2241485c7e02498ff19ddc72a11d8ac3ec814e53f69978c5f", "pins" : [ { "identity" : "swift-algorithms", diff --git a/Package.swift b/Package.swift index 59ac3c97..a248c7bf 100644 --- a/Package.swift +++ b/Package.swift @@ -102,6 +102,7 @@ let package = Package( .target( name: "HomomorphicEncryption", dependencies: [ + .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), .product(name: "Crypto", package: "swift-crypto"), .product(name: "_CryptoExtras", package: "swift-crypto"), "CUtil", diff --git a/Sources/HomomorphicEncryption/Ciphertext.swift b/Sources/HomomorphicEncryption/Ciphertext.swift index 9219f49b..48e64102 100644 --- a/Sources/HomomorphicEncryption/Ciphertext.swift +++ b/Sources/HomomorphicEncryption/Ciphertext.swift @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +public import AsyncAlgorithms + /// Ciphertext type. public struct Ciphertext: Equatable, Sendable { public typealias Scalar = Scheme.Scalar @@ -28,7 +30,7 @@ public struct Ciphertext: Equatable, Senda /// /// After a fresh encryption, the ciphertext has ``HeScheme/freshCiphertextPolyCount`` polynomials. /// The count may change during the course of HE operations, e.g. increase during ciphertext multiplication, - /// or decrease during relinearization ``Ciphertext/relinearize(using:)``. + /// or decrease during relinearization ``Ciphertext/relinearize(using:)-41bsm``. public var polyCount: Int { polys.count } @@ -314,7 +316,7 @@ public struct Ciphertext: Equatable, Senda /// /// If the ciphertext already has a single modulus, this is a no-op. /// - Throws: Error upon failure to modulus switch. - /// - seealso: ``Ciphertext/modSwitchDown()`` for more information and an alternative API. + /// - seealso: ``Ciphertext/modSwitchDown()-4an2b`` for more information and an alternative API. @inlinable public mutating func modSwitchDownToSingle() throws where Format == Scheme.CanonicalCiphertextFormat { try Scheme.modSwitchDownToSingle(&self) @@ -535,22 +537,31 @@ extension Ciphertext where Format == Scheme.CanonicalCiphertextFormat { } extension Collection { + /// Sums together the ciphertexts in the collection. + /// - Throws: Precondition failure if the collection is empty. + /// - Returns: The sum. @inlinable - func sum() throws -> Element where Element == Ciphertext { + public func sum() throws -> Element where Element == Ciphertext { precondition(!isEmpty) // swiftlint:disable:next force_unwrapping return try dropFirst().reduce(first!) { try $0 + $1 } } + /// Sums together the ciphertexts in the collection. + /// - Throws: Precondition failure if the collection is empty. + /// - Returns: The sum. @inlinable - func sum() throws -> Element where Element == Ciphertext { + public func sum() throws -> Element where Element == Ciphertext { precondition(!isEmpty) // swiftlint:disable:next force_unwrapping return try dropFirst().reduce(first!) { try $0 + $1 } } + /// Sums together the ciphertexts in the collection. + /// - Throws: Precondition failure if the collection is empty. + /// - Returns: The sum. @inlinable - func sum() throws -> Element where Element == Ciphertext { + public func sum() throws -> Element where Element == Ciphertext { precondition(!isEmpty) // swiftlint:disable:next force_unwrapping return try dropFirst().reduce(first!) { try $0 + $1 } @@ -937,4 +948,167 @@ extension Ciphertext { } throw HeError.errorCastingPolyFormat(from: Format.self, to: Scheme.CanonicalCiphertextFormat.self) } + + // MARK: Async rotations + + /// Asynchronously rotates the columns of a ciphertext. + /// + /// - Parameters: + /// - step: Number of slots to rotate. Negative values indicate a left rotation, and positive values indicate a + /// right rotation. Must have absolute value in `[1, N / 2 - 1]` where `N` is the RLWE ring dimension, given by + /// ``EncryptionParameters/polyDegree``. + /// - evaluationKey: Evaluation key to use in the HE computation. Must contain the Galois element associated with + /// `step`, see ``GaloisElement/rotatingColumns(by:degree:)``. + /// - Throws: failure to rotate ciphertext's columns. + /// - seealso: ``HeScheme/rotateColumns(of:by:using:)-7h3fz`` for an alternate API and more information. + @inlinable + public mutating func rotateColumns(by step: Int, + using evaluationKey: EvaluationKey) async throws + where Format == Scheme.CanonicalCiphertextFormat + { + try await Scheme.rotateColumnsAsync(of: &self, by: step, using: evaluationKey) + } + + /// Asynchronously swaps the rows of a ciphertext. + /// + /// A plaintext in ``EncodeFormat/simd`` format can be viewed a `2 x (N / 2)` matrix of coefficients. + /// For instance, for `N = 8`, given a ciphertext encrypting a plaintext with values + /// ``` + /// [1, 2, 3, 4, 5, 6, 7, 8] + /// ``` + /// calling ``HeScheme/swapRows(of:using:)`` with `step: 1` will yield a ciphertext decrypting to + /// ``` + /// [5, 6, 7, 8, 1, 2, 3, 4] + /// ``` + /// - Parameter evaluationKey: Evaluation key to use in the HE computation. Must contain the Galois element + /// associated with `step`, see ``GaloisElement/rotatingColumns(by:degree:)``. + /// - Throws: error upon failure to swap the ciphertext's rows. + /// - seealso: ``HeScheme/swapRows(of:using:)-50tac`` for an alternate API. + @inlinable + public mutating func swapRows(using evaluationKey: EvaluationKey) async throws + where Format == Scheme.CanonicalCiphertextFormat + { + try await Scheme.swapRowsAsync(of: &self, using: evaluationKey) + } + + /// Asynchronously performs modulus switching on the ciphertext. + /// + /// - Throws: Error upon failure to mod-switch. + /// - seealso: ``HeScheme/modSwitchDown(_:)`` for an alternative API and more information. + @inlinable + public mutating func modSwitchDown() async throws where Format == Scheme.CanonicalCiphertextFormat { + try await Scheme.modSwitchDownAsync(&self) + } + + /// Asynchronously performs modulus switching to a single modulus. + /// + /// If the ciphertext already has a single modulus, this is a no-op. + /// - Throws: Error upon failure to modulus switch. + /// - seealso: ``Ciphertext/modSwitchDown()-4an2b`` for more information and an alternative API. + @inlinable + public mutating func modSwitchDownToSingle() async throws where Format == Scheme.CanonicalCiphertextFormat { + try await Scheme.modSwitchDownToSingleAsync(&self) + } +} + +extension Ciphertext where Format == Scheme.CanonicalCiphertextFormat { + /// Asynchronously applies a Galois transformation. + /// + /// - Parameters: + /// - element: Galois element of the transformation. Must be odd in `[1, 2 * N - 1]` where `N` is the RLWE ring + /// dimension, given by ``EncryptionParameters/polyDegree``. + /// - key: Evaluation key. Must contain Galois element `element`. + /// - Throws: Error upon failure to apply the Galois transformation. + /// - seealso: ``HeScheme/applyGalois(ciphertext:element:using:)`` for an alternative API and more information. + @inlinable + public mutating func applyGalois(element: Int, using key: EvaluationKey) async throws { + try await Scheme.applyGaloisAsync(ciphertext: &self, element: element, using: key) + } + + /// Asynchronously Relinearizes the ciphertext. + /// + /// - Parameter key: Evaluation key to relinearize with. Must contain a `RelinearizationKey`. + /// - Throws: Error upon failure to relinearize. + /// - seealso: ``HeScheme/relinearize(_:using:)`` for an alternative API and more information. + @inlinable + public mutating func relinearize(using key: EvaluationKey) async throws { + try await Scheme.relinearizeAsync(&self, using: key) + } +} + +// MARK: - Async collection extensions + +extension Collection { + /// Sums together the ciphertexts in the collection. + /// - Throws: Precondition failure if the collection is empty. + /// - Returns: The sum. + @inlinable + public func sum() async throws -> Element where Element == Ciphertext { + precondition(!isEmpty) + // swiftlint:disable:next force_unwrapping + return try await dropFirst().async.reduce(first!) { try await $0 + $1 } + } + + /// Sums together the ciphertexts in the collection. + /// - Throws: Precondition failure if the collection is empty. + /// - Returns: The sum. + @inlinable + public func sum() async throws -> Element where Element == Ciphertext { + precondition(!isEmpty) + // swiftlint:disable:next force_unwrapping + return try await dropFirst().async.reduce(first!) { try await $0 + $1 } + } + + /// Sums together the ciphertexts in the collection. + /// - Throws: Precondition failure if the collection is empty. + /// - Returns: The sum. + @inlinable + public func sum() async throws -> Element where + Element == Ciphertext + { + precondition(!isEmpty) + // swiftlint:disable:next force_unwrapping + return try await dropFirst().async.reduce(first!) { try await $0 + $1 } + } + + /// Asynchronously computes an inner product between self and a collection of (optional) plaintexts in ``Eval`` + /// format. + /// + /// The inner product encrypts `sum_{i, plaintexts[i] != nil} self[i] * plaintexts[i]`. `plaintexts[i]` + /// may be `nil`, which denotes a zero plaintext. + /// - Parameter plaintexts: Plaintexts. Must not be empty and have `count` matching `self.count`. + /// - Returns: A ciphertext encrypting the inner product. + /// - Throws: Error upon failure to compute inner product. + @inlinable + public func innerProduct(plaintexts: some Collection?>) async throws -> Element + where Element == Ciphertext + { + try await Scheme.innerProductAsync(ciphertexts: self, plaintexts: plaintexts) + } + + /// Asynchronously computes an inner product between self and a collection of plaintexts in ``Eval`` format. + /// + /// The inner product encrypts `sum_{i} self[i] * plaintexts[i]`. + /// - Parameter plaintexts: Plaintexts. Must not be empty and have `count` matching `self.count`. + /// - Returns: A ciphertext encrypting the inner product. + /// - Throws: Error upon failure to compute inner product. + @inlinable + public func innerProduct(plaintexts: some Collection>) async throws -> Element + where Element == Ciphertext + { + try await Scheme.innerProductAsync(ciphertexts: self, plaintexts: plaintexts) + } + + /// Asynchronously computes an inner product between self and another collection of ciphertexts. + /// + /// The inner product encrypts `sum_{i} self[i] * ciphertexts[i]`. + /// - Parameter ciphertexts: Ciphertexts. Must not be empty and have `count` matching `self.count`. + /// - Returns: A ciphertext encrypting the inner product. + /// - Throws: Error upon failure to compute inner product. + @inlinable + public func innerProduct(ciphertexts: some Collection) async throws -> Element + where Element == Ciphertext + { + try await Scheme.innerProductAsync(self, ciphertexts) + } } diff --git a/Sources/HomomorphicEncryption/HeScheme.swift b/Sources/HomomorphicEncryption/HeScheme.swift index db5d473a..e13c954e 100644 --- a/Sources/HomomorphicEncryption/HeScheme.swift +++ b/Sources/HomomorphicEncryption/HeScheme.swift @@ -485,7 +485,7 @@ public protocol HeScheme: Sendable { /// - evaluationKey: Evaluation key to use in the HE computation. Must contain the Galois element associated with /// `step`, see ``GaloisElement/rotatingColumns(by:degree:)``. /// - Throws: failure to rotate ciphertext's columns. - /// - seealso: ``Ciphertext/rotateColumns(by:using:)`` for an alternate API. + /// - seealso: ``Ciphertext/rotateColumns(by:using:)-4f3tp`` for an alternate API. static func rotateColumns( of ciphertext: inout CanonicalCiphertext, by step: Int, @@ -513,7 +513,7 @@ public protocol HeScheme: Sendable { /// - evaluationKey: Evaluation key to use in the HE computation. Must contain the Galois element returned from /// ``GaloisElement/swappingRows(degree:)``. /// - Throws: error upon failure to swap the ciphertext's rows. - /// - seealso: ``Ciphertext/swapRows(using:)`` for an alternate API. ``swapRowsAsync(of:using:)`` for an async + /// - seealso: ``Ciphertext/swapRows(using:)-4o179`` for an alternate API. ``swapRowsAsync(of:using:)`` for an async /// version of this API static func swapRows(of ciphertext: inout CanonicalCiphertext, using evaluationKey: EvaluationKey) throws @@ -862,7 +862,7 @@ public protocol HeScheme: Sendable { /// serialization and sending the ciphertext to the secret key owner. /// - Parameter ciphertext: Ciphertext; must have > 1 ciphertext modulus. /// - Throws: Error upon failure to mod-switch. - /// - seealso: ``Ciphertext/modSwitchDown()`` for an alternative API. + /// - seealso: ``Ciphertext/modSwitchDown()-4an2b`` for an alternative API. /// - seealso: ``modSwitchDownAsync(_:)`` for an async version of this API static func modSwitchDown(_ ciphertext: inout CanonicalCiphertext) throws @@ -873,7 +873,7 @@ public protocol HeScheme: Sendable { /// /// If the ciphertext already has a single modulus, this is a no-op. /// - Throws: Error upon failure to modulus switch. - /// - seealso: ``Ciphertext/modSwitchDownToSingle()`` for more information and an alternative API. + /// - seealso: ``Ciphertext/modSwitchDownToSingle()-3x0dy`` for more information and an alternative API. /// - seealso: ``modSwitchDownToSingleAsync(_:)`` for an async version of this API static func modSwitchDownToSingle(_ ciphertext: inout CanonicalCiphertext) throws diff --git a/Sources/PrivateInformationRetrieval/IndexPir/MulPir.swift b/Sources/PrivateInformationRetrieval/IndexPir/MulPir.swift index f41a37bd..e766a323 100644 --- a/Sources/PrivateInformationRetrieval/IndexPir/MulPir.swift +++ b/Sources/PrivateInformationRetrieval/IndexPir/MulPir.swift @@ -359,10 +359,7 @@ extension MulPirServer { let startIndex = dataChunk.startIndex + expandedDim0Query.count * columnIndex let endIndex = min(startIndex + expandedDim0Query.count, dataChunk.endIndex) let plaintexts = dataChunk[startIndex..) async throws + where Format == Scheme.CanonicalCiphertextFormat + { + try await Scheme.rotateColumnsMultiStepAsync(of: &self, by: step, using: evaluationKey) + } } diff --git a/Sources/_TestUtilities/HeApiTestUtils.swift b/Sources/_TestUtilities/HeApiTestUtils.swift index d6f2d9d4..5ce91bd8 100644 --- a/Sources/_TestUtilities/HeApiTestUtils.swift +++ b/Sources/_TestUtilities/HeApiTestUtils.swift @@ -567,7 +567,7 @@ public enum HeAPITestHelpers { try await ciphertextProduct2 *= ciphertext2 var relinearizedProd = ciphertextProduct - try relinearizedProd.relinearize(using: #require(testEnv.evaluationKey)) + try await relinearizedProd.relinearize(using: #require(testEnv.evaluationKey)) #expect(relinearizedProd.polys.count == Scheme.freshCiphertextPolyCount) let coeffCiphertext = try await ciphertextProduct.convertToCoeffFormat() @@ -595,6 +595,25 @@ public enum HeAPITestHelpers { let testEnv = try TestEnv(context: context, format: .simd) let data1 = testEnv.data1 let data2 = testEnv.data2 + + func syncTest( + ciphertexts: [Scheme.EvalCiphertext], + plaintexts: [Scheme.EvalPlaintext?], + expected: [Scheme.Scalar]) throws + { + let innerProduct = try ciphertexts.innerProduct(plaintexts: plaintexts) + try testEnv.checkDecryptsDecodes(ciphertext: innerProduct, format: .simd, expected: expected) + } + + func asyncTest( + ciphertexts: [Scheme.EvalCiphertext], + plaintexts: [Scheme.EvalPlaintext?], + expected: [Scheme.Scalar]) async throws + { + let innerProduct = try await ciphertexts.innerProduct(plaintexts: plaintexts) + try testEnv.checkDecryptsDecodes(ciphertext: innerProduct, format: .simd, expected: expected) + } + for count in [4, 1257] { let innerProductData = zip(data1, data2) .map { x, y in @@ -606,16 +625,8 @@ public enum HeAPITestHelpers { do { let ciphertexts = Array(repeating: testEnv.evalCiphertext1, count: count) let plaintexts = Array(repeating: testEnv.evalPlaintext2, count: count) - let innerProduct = try ciphertexts.innerProduct(plaintexts: plaintexts) - try testEnv.checkDecryptsDecodes(ciphertext: innerProduct, format: .simd, expected: innerProductData) - - let innerProductAsync = try await Scheme.innerProductAsync( - ciphertexts: ciphertexts, - plaintexts: plaintexts) - try testEnv.checkDecryptsDecodes( - ciphertext: innerProductAsync, - format: .simd, - expected: innerProductData) + try syncTest(ciphertexts: ciphertexts, plaintexts: plaintexts, expected: innerProductData) + try await asyncTest(ciphertexts: ciphertexts, plaintexts: plaintexts, expected: innerProductData) } // no nil values do { @@ -625,16 +636,8 @@ public enum HeAPITestHelpers { let plaintexts: [Scheme.EvalPlaintext?] = Array( repeating: testEnv.evalPlaintext2, count: count) - let innerProduct = try ciphertexts.innerProduct(plaintexts: plaintexts) - try testEnv.checkDecryptsDecodes(ciphertext: innerProduct, format: .simd, expected: innerProductData) - - let innerProductAsync = try await Scheme.innerProductAsync( - ciphertexts: ciphertexts, - plaintexts: plaintexts) - try testEnv.checkDecryptsDecodes( - ciphertext: innerProductAsync, - format: .simd, - expected: innerProductData) + try syncTest(ciphertexts: ciphertexts, plaintexts: plaintexts, expected: innerProductData) + try await asyncTest(ciphertexts: ciphertexts, plaintexts: plaintexts, expected: innerProductData) } // some nil values do { @@ -644,16 +647,8 @@ public enum HeAPITestHelpers { let plaintexts: [Scheme.EvalPlaintext?] = Array( repeating: testEnv.evalPlaintext2, count: count) + [nil] - let innerProduct = try ciphertexts.innerProduct(plaintexts: plaintexts) - try testEnv.checkDecryptsDecodes(ciphertext: innerProduct, format: .simd, expected: innerProductData) - - let innerProductAsync = try await Scheme.innerProductAsync( - ciphertexts: ciphertexts, - plaintexts: plaintexts) - try testEnv.checkDecryptsDecodes( - ciphertext: innerProductAsync, - format: .simd, - expected: innerProductData) + try syncTest(ciphertexts: ciphertexts, plaintexts: plaintexts, expected: innerProductData) + try await asyncTest(ciphertexts: ciphertexts, plaintexts: plaintexts, expected: innerProductData) } } } @@ -676,10 +671,17 @@ public enum HeAPITestHelpers { } let ciphers1 = Array(repeating: testEnv.ciphertext1, count: count) let ciphers2 = Array(repeating: testEnv.ciphertext2, count: count) - let innerProduct = try ciphers1.innerProduct(ciphertexts: ciphers2) - let innerProductAsync = try await Scheme.innerProductAsync(ciphers1, ciphers2) - try testEnv.checkDecryptsDecodes(ciphertext: innerProduct, format: .simd, expected: innerProductData) - try testEnv.checkDecryptsDecodes(ciphertext: innerProductAsync, format: .simd, expected: innerProductData) + func syncTests() throws { + let innerProduct = try ciphers1.innerProduct(ciphertexts: ciphers2) + try testEnv.checkDecryptsDecodes(ciphertext: innerProduct, format: .simd, expected: innerProductData) + } + try syncTests() + + func asyncTests() async throws { + let innerProduct = try await ciphers1.innerProduct(ciphertexts: ciphers2) + try testEnv.checkDecryptsDecodes(ciphertext: innerProduct, format: .simd, expected: innerProductData) + } + try await asyncTests() } } @@ -1233,7 +1235,7 @@ public enum HeAPITestHelpers { // with mod-switch down if context.coefficientModuli.count > 2 { var ciphertext = testEnv.ciphertext1 - try ciphertext.modSwitchDown() + try await ciphertext.modSwitchDown() let evalCiphertext = try await ciphertext.convertToEvalFormat() let moduliCount = evalCiphertext.moduli.count let evalPlaintext = try testEnv.context.encode(values: data2, format: .simd, moduliCount: moduliCount) @@ -1253,48 +1255,53 @@ public enum HeAPITestHelpers { context: Scheme.Context, scheme _: Scheme.Type) async throws { - func runRotationTest(context: Scheme.Context, galoisElements: [Int], multiStep: Bool) async throws { - let degree = context.degree + func runRotationTestSync(context: Scheme.Context, galoisElements: [Int], multiStep: Bool) throws { let testEnv = try TestEnv(context: context, format: .simd, galoisElements: galoisElements) let evaluationKey = try #require(testEnv.evaluationKey) + let degree = context.degree for step in 1..(context: context, format: .simd, galoisElements: galoisElements) + let evaluationKey = try #require(testEnv.evaluationKey) + let degree = context.degree + for step in 1..> 1)).flatMap { step in + try [ + GaloisElement.rotatingColumns(by: step, degree: degree), + GaloisElement.rotatingColumns(by: -step, degree: degree), + ] + } + let galoisElementsMultiStep = try GaloisElement.rotatingColumnsMultiStep(degree: degree) + + try runRotationTestSync(context: context, galoisElements: galoisElementsRotate, multiStep: false) + try await runRotationTestAsync(context: context, galoisElements: galoisElementsMultiStep, multiStep: true) + } + + /// Testing ciphertext rotation of the scheme. + @inlinable + public static func schemeSwapRowsTest( + context: Scheme.Context, + scheme _: Scheme.Type) async throws + { + guard context.supportsSimdEncoding, context.supportsEvaluationKey else { + return + } let degree = context.degree let galoisElementsSwap = [GaloisElement.swappingRows(degree: degree)] let testEnv = try TestEnv(context: context, format: .simd, galoisElements: galoisElementsSwap) let evaluationKey = try #require(testEnv.evaluationKey) let expectedData = Array(testEnv.data1[degree / 2..> 1)).flatMap { step in - try [ - GaloisElement.rotatingColumns(by: step, degree: degree), - GaloisElement.rotatingColumns(by: -step, degree: degree), - ] + try await ciphertext.swapRows(using: evaluationKey) + try testEnv.checkDecryptsDecodes(ciphertext: ciphertext, format: .simd, expected: testEnv.data1) } - let galoisElementsMultiStep = try GaloisElement.rotatingColumnsMultiStep(degree: degree) - - try await runRotationTest(context: context, galoisElements: galoisElementsRotate, multiStep: false) - try await runRotationTest(context: context, galoisElements: galoisElementsMultiStep, multiStep: true) + try await swapRowsAsyncTest() } /// Testing apply Galois element of the scheme. @@ -1358,25 +1382,37 @@ public enum HeAPITestHelpers { let dataCount = testEnv.data1.count let halfDataCount = dataCount / 2 - for (step, element) in elements.enumerated() { - for modSwitchCount in 0...max(0, context.coefficientModuli.count - 2) { - var rotatedCiphertext = testEnv.ciphertext1 - var rotatedCiphertextAsync = testEnv.ciphertext1 + func syncTest() throws { + for (step, element) in elements.enumerated() { + for modSwitchCount in 0...max(0, context.coefficientModuli.count - 2) { + var rotatedCiphertext = testEnv.ciphertext1 + + for _ in 0..(context: context, format: .coefficient) - - var coeffCiphertext = testEnv.ciphertext1 - var coeffCiphertextAsync = try await coeffCiphertext.convertToCoeffFormat() let expected = testEnv.data1.map { num in num.multiplyMod(Scheme.Scalar(5), modulus: testEnv.context.plaintextModulus, variableTime: true) } func syncTest() throws { + var coeffCiphertext = testEnv.ciphertext1 try coeffCiphertext += testEnv.coeffPlaintext1 try coeffCiphertext += testEnv.ciphertext1 try coeffCiphertext += testEnv.coeffPlaintext1 @@ -1449,11 +1483,44 @@ public enum HeAPITestHelpers { try syncTest() func asyncTest() async throws { - try await coeffCiphertextAsync += testEnv.coeffPlaintext1 - try await coeffCiphertextAsync += testEnv.ciphertext1 - try await coeffCiphertextAsync += testEnv.coeffPlaintext1 - try await coeffCiphertextAsync += testEnv.coeffPlaintext1 - try testEnv.checkDecryptsDecodes(ciphertext: coeffCiphertextAsync, format: .coefficient, expected: expected) + var coeffCiphertext = testEnv.ciphertext1 + try await coeffCiphertext += testEnv.coeffPlaintext1 + try await coeffCiphertext += testEnv.ciphertext1 + try await coeffCiphertext += testEnv.coeffPlaintext1 + try await coeffCiphertext += testEnv.coeffPlaintext1 + try testEnv.checkDecryptsDecodes(ciphertext: coeffCiphertext, format: .coefficient, expected: expected) + } + try await asyncTest() + } + + /// Testing repeated addition. + @inlinable + public static func schemeSumTest( + context: Scheme.Context, + scheme _: Scheme.Type) async throws + { + let testEnv = try HeAPITestHelpers.TestEnv(context: context, format: .coefficient) + let coeffCiphertext = try await testEnv.ciphertext1.convertToCoeffFormat() + let coeffCiphertexts = Array(repeating: coeffCiphertext, count: 5) + let evalCiphertext = testEnv.evalCiphertext1 + let evalCiphertexts = Array(repeating: evalCiphertext, count: 5) + let expected = testEnv.data1.map { num in + num.multiplyMod(Scheme.Scalar(5), modulus: testEnv.context.plaintextModulus, variableTime: true) + } + + func syncTest() throws { + let coeffSum = try coeffCiphertexts.sum() + let evalSum = try evalCiphertexts.sum() + try testEnv.checkDecryptsDecodes(ciphertext: coeffSum, format: .coefficient, expected: expected) + try testEnv.checkDecryptsDecodes(ciphertext: evalSum, format: .coefficient, expected: expected) + } + try syncTest() + + func asyncTest() async throws { + let coeffSum = try await coeffCiphertexts.sum() + let evalSum = try await evalCiphertexts.sum() + try testEnv.checkDecryptsDecodes(ciphertext: coeffSum, format: .coefficient, expected: expected) + try testEnv.checkDecryptsDecodes(ciphertext: evalSum, format: .coefficient, expected: expected) } try await asyncTest() } diff --git a/Tests/HomomorphicEncryptionTests/HeAPITests.swift b/Tests/HomomorphicEncryptionTests/HeAPITests.swift index 4fb1b3ed..eef308a0 100644 --- a/Tests/HomomorphicEncryptionTests/HeAPITests.swift +++ b/Tests/HomomorphicEncryptionTests/HeAPITests.swift @@ -109,6 +109,8 @@ struct HeAPITests { try await HeAPITestHelpers.schemeCiphertextMultiplySubTest(context: context, scheme: NoOpScheme.self) try await HeAPITestHelpers.schemeCiphertextNegateTest(context: context, scheme: NoOpScheme.self) try await HeAPITestHelpers.schemeRotationTest(context: context, scheme: NoOpScheme.self) + try await HeAPITestHelpers.schemeSwapRowsTest(context: context, scheme: NoOpScheme.self) + try await HeAPITestHelpers.schemeSumTest(context: context, scheme: NoOpScheme.self) try await HeAPITestHelpers.schemeApplyGaloisTest(context: context, scheme: NoOpScheme.self) // swiftlint:enable line_length // swiftformat:enable wrap wrapArguments @@ -206,7 +208,9 @@ struct HeAPITests { try await HeAPITestHelpers.schemeCiphertextNegateTest(context: context, scheme: Bfv.self) try await HeAPITestHelpers.schemeApplyGaloisTest(context: context, scheme: Bfv.self) try await HeAPITestHelpers.schemeRotationTest(context: context, scheme: Bfv.self) + try await HeAPITestHelpers.schemeSwapRowsTest(context: context, scheme: Bfv.self) try await HeAPITestHelpers.repeatedAdditionTest(context: context, scheme: Bfv.self) + try await HeAPITestHelpers.schemeSumTest(context: context, scheme: Bfv.self) try await HeAPITestHelpers.multiplyPowerOfXTest(context: context, scheme: Bfv.self) try await HeAPITestHelpers.schemeTestNtt(context: context, scheme: Bfv.self) try await HeAPITestHelpers.schemeTestFormats(context: context, scheme: Bfv.self)