diff --git a/Sources/HomomorphicEncryption/Bfv/Bfv.swift b/Sources/HomomorphicEncryption/Bfv/Bfv.swift index 7798a714..13ac0981 100644 --- a/Sources/HomomorphicEncryption/Bfv/Bfv.swift +++ b/Sources/HomomorphicEncryption/Bfv/Bfv.swift @@ -1,4 +1,4 @@ -// Copyright 2024 Apple Inc. and the Swift Homomorphic Encryption project authors +// Copyright 2024-2025 Apple Inc. and the Swift Homomorphic Encryption project authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -339,4 +339,22 @@ public enum Bfv: HeScheme { reduceToCiphertext(accumulator: accumulator, result: &result) return result } + + @inlinable + public static func forwardNtt(_ ciphertext: CoeffCiphertext) throws -> EvalCiphertext { + let polys = try ciphertext.polys.map { try $0.forwardNtt() } + return Ciphertext, Eval>(context: ciphertext.context, + polys: polys, + correctionFactor: ciphertext.correctionFactor, + seed: ciphertext.seed) + } + + @inlinable + public static func inverseNtt(_ ciphertext: EvalCiphertext) throws -> CoeffCiphertext { + let polys = try ciphertext.polys.map { try $0.inverseNtt() } + return Ciphertext, Coeff>(context: ciphertext.context, + polys: polys, + correctionFactor: ciphertext.correctionFactor, + seed: ciphertext.seed) + } } diff --git a/Sources/HomomorphicEncryption/Ciphertext.swift b/Sources/HomomorphicEncryption/Ciphertext.swift index 63ac13f6..f7e34d69 100644 --- a/Sources/HomomorphicEncryption/Ciphertext.swift +++ b/Sources/HomomorphicEncryption/Ciphertext.swift @@ -162,14 +162,12 @@ public struct Ciphertext: Equatable, Senda @inlinable package func forwardNtt() throws -> Ciphertext where Format == Coeff { - let polys = try polys.map { try $0.forwardNtt() } - return Ciphertext(context: context, polys: polys, correctionFactor: correctionFactor, seed: seed) + try Scheme.forwardNtt(self) } @inlinable package func inverseNtt() throws -> Ciphertext where Format == Eval { - let polys = try polys.map { try $0.inverseNtt() } - return Ciphertext(context: context, polys: polys, correctionFactor: correctionFactor, seed: seed) + try Scheme.inverseNtt(self) } /// Converts the ciphertext to a ``HeScheme/CoeffCiphertext``. @@ -296,9 +294,7 @@ public struct Ciphertext: Equatable, Senda /// - seealso: ``Ciphertext/modSwitchDown()`` for more information and an alternative API. @inlinable public mutating func modSwitchDownToSingle() throws where Format == Scheme.CanonicalCiphertextFormat { - while moduli.count > 1 { - try Scheme.modSwitchDown(&self) - } + try Scheme.modSwitchDownToSingle(&self) } /// Decryption of a ciphertext. @@ -485,10 +481,7 @@ extension Ciphertext where Format == Coeff { /// - Throws: Error upon failure to compute the inverse. @inlinable public mutating func multiplyInversePowerOfX(power: Int) throws { - precondition(power >= 0) - for index in polys.indices { - try polys[index].multiplyInversePowerOfX(power) - } + try Scheme.multiplyInversePowerOfX(&self, power: power) } } diff --git a/Sources/HomomorphicEncryption/HeScheme.swift b/Sources/HomomorphicEncryption/HeScheme.swift index 3065726b..088218a7 100644 --- a/Sources/HomomorphicEncryption/HeScheme.swift +++ b/Sources/HomomorphicEncryption/HeScheme.swift @@ -378,6 +378,12 @@ public protocol HeScheme { by step: Int, using evaluationKey: EvaluationKey) throws + /// The async version of ``HeScheme/rotateColumns(of:by:using:)`` + static func rotateColumnsAsync( + of ciphertext: inout CanonicalCiphertext, + by step: Int, + using evaluationKey: EvaluationKey) async throws + /// Swaps the rows of a ciphertext. /// /// A plaintext in ``EncodeFormat/simd`` format can be viewed a `2 x (N / 2)` matrix of coefficients. @@ -394,84 +400,136 @@ public protocol HeScheme { /// - 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. + /// - seealso: ``Ciphertext/swapRows(using:)`` 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 + /// The async version of ``HeScheme/swapRows(of:using:)`` + static func swapRowsAsync(of ciphertext: inout CanonicalCiphertext, using evaluationKey: EvaluationKey) async throws + /// In-place plaintext addition: `lhs += rhs`. /// - Parameters: /// - lhs: Plaintext to add; will store the sum. /// - rhs: Plaintext to add. /// - Throws: Error upon failure to add. + /// - seealso: for an async version + /// of this API static func addAssign(_ lhs: inout CoeffPlaintext, _ rhs: CoeffPlaintext) throws + /// The async version of ``HeScheme/addAssign(_:_:)-3bv7g`` + static func addAssignAsync(_ lhs: inout CoeffPlaintext, _ rhs: CoeffPlaintext) async throws + /// In-place plaintext addition: `lhs += rhs`. /// - Parameters: /// - lhs: Plaintext to add; will store the sum. /// - rhs: Plaintext to add. /// - Throws: Error upon failure to add. + /// - seealso: for an async version + /// of this API static func addAssign(_ lhs: inout EvalPlaintext, _ rhs: EvalPlaintext) throws + /// The async version of ``HeScheme/addAssign(_:_:)-1osb9`` + static func addAssignAsync(_ lhs: inout EvalPlaintext, _ rhs: EvalPlaintext) async throws + /// In-place ciphertext addition: `lhs += rhs`. /// - Parameters: /// - lhs: Ciphertext to add; will store the sum. /// - rhs: Ciphertext to add. /// - Throws: Error upon failure to add. + /// - seealso: for an async + /// version of this API static func addAssignCoeff(_ lhs: inout CoeffCiphertext, _ rhs: CoeffCiphertext) throws + /// The async version of ``HeScheme/addAssignCoeff(_:_:)-96q8a``. + static func addAssignCoeffAsync(_ lhs: inout CoeffCiphertext, _ rhs: CoeffCiphertext) async throws + /// In-place ciphertext addition: `lhs += rhs`. /// - Parameters: /// - lhs: Ciphertext to add; will store the sum. /// - rhs: Ciphertext to add. /// - Throws: Error upon failure to add. + /// - seealso: for an async + /// version of this API static func addAssignEval(_ lhs: inout EvalCiphertext, _ rhs: EvalCiphertext) throws + /// The async version of ``HeScheme/addAssignEval(_:_:)-6rg4i``. + static func addAssignEvalAsync(_ lhs: inout EvalCiphertext, _ rhs: EvalCiphertext) async throws + /// In-place ciphertext subtraction: `lhs -= rhs`. /// - Parameters: /// - lhs: Ciphertext to subtract from; will store the difference. /// - rhs: Ciphertext to subtract. /// - Throws: Error upon failure to subtract. + /// - seealso: ``subAssignCoeffAsync(_:_:) for an async version of this API static func subAssignCoeff(_ lhs: inout CoeffCiphertext, _ rhs: CoeffCiphertext) throws + /// The async version of ``HeScheme/subAssignCoeff(_:_:)-7ae21``. + static func subAssignCoeffAsync(_ lhs: inout CoeffCiphertext, _ rhs: CoeffCiphertext) async throws + /// In-place ciphertext subtraction: `lhs -= rhs`. /// /// - Parameters: /// - lhs: Ciphertext to subtract from; will store the difference. /// - rhs: Ciphertext to subtract. /// - Throws: Error upon failure to subtract. + /// - seealso: ``subAssignEval(_:_:)`` for an async version of this API static func subAssignEval(_ lhs: inout EvalCiphertext, _ rhs: EvalCiphertext) throws + /// The async version of ``HeScheme/subAssignEval(_:_:)-17q3d``. + static func subAssignEvalAsync(_ lhs: inout EvalCiphertext, _ rhs: EvalCiphertext) async throws + /// In-place ciphertext-plaintext addition: `ciphertext += plaintext`. /// /// - Parameters: /// - ciphertext: Ciphertext to add; will store the sum. /// - plaintext: Plaintext to add. /// - Throws: Error upon failure to add. + /// - seealso: for an async + /// version of this API static func addAssignCoeff(_ ciphertext: inout CoeffCiphertext, _ plaintext: CoeffPlaintext) throws + /// The async version of ``HeScheme/addAssignCoeff(_:_:)-3zekp``. + static func addAssignCoeffAsync(_ ciphertext: inout CoeffCiphertext, _ plaintext: CoeffPlaintext) async throws + /// In-place ciphertext-plaintext addition: `ciphertext += plaintext`. /// /// - Parameters: /// - ciphertext: Ciphertext to add; will store the sum. /// - plaintext: Plaintext to add. /// - Throws: Error upon failure to add. + /// - seealso: for an async + /// version of this API static func addAssignEval(_ ciphertext: inout EvalCiphertext, _ plaintext: EvalPlaintext) throws + /// The async version of ``HeScheme/addAssignEval(_:_:)-5r98u``. + static func addAssignEvalAsync(_ ciphertext: inout EvalCiphertext, _ plaintext: EvalPlaintext) async throws + /// In-place ciphertext-plaintext subtraction: `ciphertext -= plaintext`. /// /// - Parameters: /// - ciphertext: Ciphertext to subtract from; will store the difference. /// - plaintext: Plaintext to subtract. /// - Throws: Error upon failure to subtract. + /// - seealso: for an async + /// version of this API static func subAssignCoeff(_ ciphertext: inout CoeffCiphertext, _ plaintext: CoeffPlaintext) throws + /// The async version of ``HeScheme/subAssignCoeff(_:_:)-168hp``. + static func subAssignCoeffAsync(_ ciphertext: inout CoeffCiphertext, _ plaintext: CoeffPlaintext) async throws + /// In-place ciphertext-plaintext subtraction: `ciphertext -= plaintext`. /// /// - Parameters: /// - ciphertext: Ciphertext to subtract from; will store the difference. /// - plaintext: Plaintext to subtract. /// - Throws: Error upon failure to subtract. + /// - seealso: for an async + /// version of this API static func subAssignEval(_ ciphertext: inout EvalCiphertext, _ plaintext: EvalPlaintext) throws + /// The async version of ``HeScheme/subAssignEval(_:_:)-1x0fw``. + static func subAssignEvalAsync(_ ciphertext: inout EvalCiphertext, _ plaintext: EvalPlaintext) async throws + /// Plaintext-ciphertext subtraction: `plaintext - ciphertext`. /// /// - Parameters: @@ -479,8 +537,13 @@ public protocol HeScheme { /// - ciphertext: Ciphertext to subtract. /// - Returns: A ciphertext encrypting the difference. /// - Throws: Error upon failure to subtract. + /// - seealso: ``subCoeffAsync(_:_:)`` for an async version of this API static func subCoeff(_ plaintext: CoeffPlaintext, _ ciphertext: CoeffCiphertext) throws -> CoeffCiphertext + /// The async version of ``HeScheme/subCoeff(_:_:)``. + static func subCoeffAsync(_ plaintext: CoeffPlaintext, _ ciphertext: CoeffCiphertext) async throws + -> CoeffCiphertext + /// Plaintext-ciphertext subtraction: `plaintext - ciphertext`. /// /// - Parameters: @@ -488,26 +551,43 @@ public protocol HeScheme { /// - ciphertext: Ciphertext to subtract. /// - Returns: A ciphertext encrypting the difference. /// - Throws: Error upon failure to subtract. + /// - seealso: ``subEvalAsync(_:_:)`` for an async version of this API static func subEval(_ plaintext: EvalPlaintext, _ ciphertext: EvalCiphertext) throws -> EvalCiphertext + /// The async version of ``HeScheme/subEval(_:_:)``. + static func subEvalAsync(_ plaintext: EvalPlaintext, _ ciphertext: EvalCiphertext) async throws -> EvalCiphertext + /// In-place ciphertext-plaintext multiplication: `ciphertext *= plaintext`. /// /// - Parameters: /// - ciphertext: Ciphertext to multiply; will store the product. /// - plaintext: Plaintext to multiply. /// - Throws: Error upon failure to multiply. + /// - seealso: for an async version + /// of this API static func mulAssign(_ ciphertext: inout EvalCiphertext, _ plaintext: EvalPlaintext) throws + /// The async version of ``HeScheme/mulAssign(_:_:)-erpv``. + static func mulAssignAsync(_ ciphertext: inout EvalCiphertext, _ plaintext: EvalPlaintext) async throws + /// In-place ciphertext negation: `ciphertext = -ciphertext`. /// /// - Parameter ciphertext: Ciphertext to negate. + /// - seealso: ``negAssignCoeffAsync(_:)`` for an async version of this API static func negAssignCoeff(_ ciphertext: inout CoeffCiphertext) + /// The async version of ``HeScheme/negAssignCoeff(_:)``. + static func negAssignCoeffAsync(_ ciphertext: inout CoeffCiphertext) async + /// In-place ciphertext negation: `ciphertext = -ciphertext`. /// /// - Parameter ciphertext: Ciphertext to negate. + /// - seealso: ``negAssignEvalAsync(_:)`` for an async version of this API static func negAssignEval(_ ciphertext: inout EvalCiphertext) + /// The async version of ``HeScheme/negAssignEval(_:)``. + static func negAssignEvalAsync(_ ciphertext: inout EvalCiphertext) async + /// Computes an inner product between two collections of ciphertexts. /// /// The inner product encrypts `sum_{i} lhs[i] * rhs[i]`. @@ -516,11 +596,18 @@ public protocol HeScheme { /// - rhs: Ciphertexts. Must not be empty and have `count` matching `lhs.count`. /// - Returns: A ciphertext encrypting the inner product. /// - Throws: Error upon failure to compute inner product. + /// - seealso: ``innerProductAsync(_:_:)-872yt for an async version of this API static func innerProduct( _ lhs: some Collection, _ rhs: some Collection) throws -> CanonicalCiphertext + /// The async version of ``HeScheme/innerProduct(_:_:)-52rbh``. + static func innerProductAsync( + _ lhs: some Collection, + _ rhs: some Collection) async throws + -> CanonicalCiphertext + /// Computes an inner product between two collections. /// /// The inner product encrypts `sum_{i} ciphertexts[i] * plaintexts[i]`. @@ -530,9 +617,14 @@ public protocol HeScheme { /// `ciphertexts.count`. /// - Returns: A ciphertext encrypting the inner product. /// - Throws: Error upon failure to compute inner product. + /// - seealso: ``innerProductAsync(ciphertexts:plaintexts:) for an async version of this API static func innerProduct(ciphertexts: some Collection, plaintexts: some Collection) throws -> EvalCiphertext + /// The async version of ``HeScheme/innerProduct(ciphertexts:plaintexts:)-93qj9``. + static func innerProductAsync(ciphertexts: some Collection, + plaintexts: some Collection) async throws -> EvalCiphertext + /// Computes an inner product between two collections. /// /// The inner product encrypts `sum_{i, plaintexts[i] != nil} self[i] * plaintexts[i]`. `plaintexts[i]` @@ -543,9 +635,15 @@ public protocol HeScheme { /// `ciphertexts.count`. `nil` plaintexts indicate zero plaintexts which can be ignored in the computation. /// - Returns: A ciphertext encrypting the inner product. /// - Throws: Error upon failure to compute inner product. + /// - seealso: + /// for an async version of this API static func innerProduct(ciphertexts: some Collection, plaintexts: some Collection) throws -> EvalCiphertext + /// The async version of ``HeScheme/innerProduct(ciphertexts:plaintexts:)-1x1ft``. + static func innerProductAsync(ciphertexts: some Collection, + plaintexts: some Collection) async throws -> EvalCiphertext + /// In-place ciphertext multiplication: `ciphertext *= ciphertext`. /// /// - Parameters: @@ -557,40 +655,62 @@ public protocol HeScheme { /// /// > Important: The resulting ciphertext has 3 polynomials and can be relinearized. See /// ``HeScheme/relinearize(_:using:)`` + /// - seealso: for an async version + /// of this API static func mulAssign(_ lhs: inout CanonicalCiphertext, _ rhs: CanonicalCiphertext) throws + /// The async version of ``HeScheme/mulAssign(_:_:)-4661e``. + static func mulAssignAsync(_ lhs: inout CanonicalCiphertext, _ rhs: CanonicalCiphertext) async throws + /// In-place ciphertext addition: `lhs += rhs`. /// /// - Parameters: /// - lhs: Ciphertext to add; will store the sum. /// - rhs: Ciphertext to add. /// - Throws: Error upon failure to add. + /// - seealso: for an async version + /// of this API static func addAssign(_ lhs: inout CanonicalCiphertext, _ rhs: CanonicalCiphertext) throws + /// The async version of ``HeScheme/addAssign(_:_:)-3z4tj``. + static func addAssignAsync(_ lhs: inout CanonicalCiphertext, _ rhs: CanonicalCiphertext) async throws + /// In-place ciphertext subtraction: `lhs -= rhs`. /// /// - Parameters: /// - lhs: Ciphertext to subtract from; will store the difference. /// - rhs: Ciphertext to subtract.. /// - Throws: Error upon failure to subtract. + /// - seealso: ``subAssignAsync(_:_:)-22pfg`` for an async version of this API static func subAssign(_ lhs: inout CanonicalCiphertext, _ rhs: CanonicalCiphertext) throws + /// The async version of ``HeScheme/subAssign(_:_:)-8givj``. + static func subAssignAsync(_ lhs: inout CanonicalCiphertext, _ rhs: CanonicalCiphertext) async throws + /// In-place ciphertext-plaintext subtraction: `ciphertext -= plaintext`. /// /// - Parameters: /// - ciphertext: Ciphertext to subtract from; will store the difference. /// - plaintext: Plaintext to subtract. /// - Throws: Error upon failure to subtract. + /// - seealso: ``subAssignAsync(_:_:)-5w3rx`` for an async version of this API static func subAssign(_ ciphertext: inout CanonicalCiphertext, _ plaintext: CoeffPlaintext) throws + /// The async version of ``HeScheme/subAssign(_:_:)-1g8oj) + static func subAssignAsync(_ ciphertext: inout CanonicalCiphertext, _ plaintext: CoeffPlaintext) async throws + /// In-place ciphertext-plaintext subtraction: `ciphertext -= plaintext`. /// /// - Parameters: /// - ciphertext: Ciphertext to subtract from; will store the difference. /// - plaintext: Plaintext to subtract. /// - Throws: Error upon failure to subtract. + /// - seealso: ``subAssignAsync(_:_:)-3fg40`` for an async version of this API static func subAssign(_ ciphertext: inout CanonicalCiphertext, _ plaintext: EvalPlaintext) throws + /// The async version of ``HeScheme/subAssign(_:_:)-5wdxi``. + static func subAssignAsync(_ ciphertext: inout CanonicalCiphertext, _ plaintext: EvalPlaintext) async throws + /// Plaintext-ciphertext subtraction: `plaintext - ciphertext`. /// /// - Parameters: @@ -598,8 +718,13 @@ public protocol HeScheme { /// - ciphertext: Ciphertext to subtract. /// - Returns: A ciphertext encrypting the difference. /// - Throws: Error upon failure to subtract. + /// - seealso: ``subAsync(_:_:)-1t0uj`` for an async version of this API static func sub(_ plaintext: CoeffPlaintext, _ ciphertext: CanonicalCiphertext) throws -> CanonicalCiphertext + /// The async version of ``HeScheme/sub(_:_:)-2hy5s``. + static func subAsync(_ plaintext: CoeffPlaintext, _ ciphertext: CanonicalCiphertext) async throws + -> CanonicalCiphertext + /// Plaintext-ciphertext subtraction: `plaintext - ciphertext`. /// /// - Parameters: @@ -607,8 +732,13 @@ public protocol HeScheme { /// - ciphertext: Ciphertext to subtract. /// - Returns: A ciphertext encrypting the difference. /// - Throws: Error upon failure to subtract. + /// - seealso: ``subAsync(_:_:)-1dv0q`` for an async version of this API static func sub(_ plaintext: EvalPlaintext, _ ciphertext: CanonicalCiphertext) throws -> CanonicalCiphertext + /// The async version of ``HeScheme/sub(_:_:)-4zldp``. + static func subAsync(_ plaintext: EvalPlaintext, _ ciphertext: CanonicalCiphertext) async throws + -> CanonicalCiphertext + /// Performs modulus switching on the ciphertext. /// /// Modulus switching drops the last coefficient modulus in the ciphertext's current ciphertext modulus, without @@ -620,8 +750,23 @@ public protocol HeScheme { /// - Parameter ciphertext: Ciphertext; must have > 1 ciphertext modulus. /// - Throws: Error upon failure to mod-switch. /// - seealso: ``Ciphertext/modSwitchDown()`` for an alternative API. + /// - seealso: ``modSwitchDownAsync(_:)`` for an async version of this API static func modSwitchDown(_ ciphertext: inout CanonicalCiphertext) throws + /// The async version of ``HeScheme/modSwitchDown(_:)``. + static func modSwitchDownAsync(_ ciphertext: inout CanonicalCiphertext) async throws + + /// 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/modSwitchDownToSingle()`` for more information and an alternative API. + /// - seealso: ``modSwitchDownToSingleAsync(_:)`` for an async version of this API + static func modSwitchDownToSingle(_ ciphertext: inout CanonicalCiphertext) throws + + /// The async version of ``HeScheme/modSwitchDownToSingle(_:)``. + static func modSwitchDownToSingleAsync(_ ciphertext: inout CanonicalCiphertext) async throws + /// Applies a Galois transformation, also known as a Frobenius transformation. /// /// The Galois transformation with Galois element `p` transforms the ciphertext encoding the polynomial `f(x)` @@ -640,11 +785,18 @@ public protocol HeScheme { /// dimension, given by ``EncryptionParameters/polyDegree``. /// - key: Evaluation key. Must contain Galois element `element`. /// - Throws: Error upon failure to apply the Galois transformation. + /// - seealso: ``applyGaloisAsync(ciphertext:element:using:)`` for an async version of this API static func applyGalois( ciphertext: inout CanonicalCiphertext, element: Int, using key: EvaluationKey) throws + /// The async version of ``HeScheme/applyGalois(ciphertext:element:using:)``. + static func applyGaloisAsync( + ciphertext: inout CanonicalCiphertext, + element: Int, + using key: EvaluationKey) async throws + /// Relinearizes a ciphertext. /// /// Relinearization reduces the number of polynomials in a ciphertext after ciphertext-ciphertext multiplication. @@ -659,8 +811,32 @@ public protocol HeScheme { /// - ciphertext: Ciphertext; must be the result of a ciphertext-ciphertext multiplication /// - key: Evaluation key; must contain a `RelinearizationKey`. /// - Throws: Error upon failure to relinearize the ciphertext. + /// - seealso: ``relinearizeAsync(_:using:)`` for an async version of this API static func relinearize(_ ciphertext: inout CanonicalCiphertext, using key: EvaluationKey) throws + /// The async version of ``HeScheme/relinearize(_:using:)``. + static func relinearizeAsync(_ ciphertext: inout CanonicalCiphertext, using key: EvaluationKey) async throws + + /// Run the forward NTT algorithm on a given ciphertext in Coeff format + /// - Parameter ciphertext: The ciphertext to run forward NTT on + /// - Returns: The corresponding ciphertext in Eval format + /// - Throws: Error upon failure to run forward NTT on the ciphertext. + /// - seealso: ``forwardNttAsync(_:)`` for an async version of this API + static func forwardNtt(_ ciphertext: CoeffCiphertext) throws -> EvalCiphertext + + /// The async version of ``HeScheme/forwardNtt(_:)``. + static func forwardNttAsync(_ ciphertext: CoeffCiphertext) async throws -> EvalCiphertext + + /// Run the inverse NTT algorithm on a given ciphertext in Eval format + /// - Parameter ciphertext: The ciphertext to run inverse NTT on + /// - Returns: The corresponding ciphertext in Coeff format + /// - Throws: Error upon failure to run inverse NTT on the ciphertext. + /// - seealso: ``inverseNttAsync(_:)`` for an async version of this API + static func inverseNtt(_ ciphertext: EvalCiphertext) throws -> CoeffCiphertext + + /// The async version of ``HeScheme/inverseNtt(_:)``. + static func inverseNttAsync(_ ciphertext: EvalCiphertext) async throws -> CoeffCiphertext + /// Validates the equality of two contexts. /// - Parameters: /// - lhs: A Context to compare. @@ -703,6 +879,29 @@ public protocol HeScheme { /// - seealso: ``Ciphertext/noiseBudget(using:variableTime:)`` for an alternative API. static func noiseBudgetEval(of ciphertext: EvalCiphertext, using secretKey: SecretKey, variableTime: Bool) throws -> Double + + /// Computes `ciphertext * x^{-power}`. + /// + /// - Parameters: + /// - ciphertext: ciphertext to be multiplied. + /// - power: Power in the monomial; must be positive. + /// - Throws: Error upon failure to compute the inverse. + /// - seealso: ``multiplyInversePowerOfXAsync(_:)`` for an async version of this API + static func multiplyInversePowerOfX(_ ciphertext: inout CoeffCiphertext, power: Int) throws + + /// The async version of ``HeScheme/multiplyInversePowerOfX(_:)``. + static func multiplyInversePowerOfXAsync(_ ciphertext: inout CoeffCiphertext, power: Int) async throws +} + +extension HeScheme { + @inlinable + // swiftlint:disable:next missing_docs attributes + public static func multiplyInversePowerOfX(_ ciphertext: inout CoeffCiphertext, power: Int) throws { + precondition(power >= 0) + for index in ciphertext.polys.indices { + try ciphertext.polys[index].multiplyInversePowerOfX(power) + } + } } extension HeScheme { @@ -1116,6 +1315,56 @@ extension HeScheme { try (0.. 1 { + try modSwitchDown(&ciphertext) + } + } } extension HeScheme { diff --git a/Sources/HomomorphicEncryption/HeSchemeAsync.swift b/Sources/HomomorphicEncryption/HeSchemeAsync.swift new file mode 100644 index 00000000..c220fbfe --- /dev/null +++ b/Sources/HomomorphicEncryption/HeSchemeAsync.swift @@ -0,0 +1,228 @@ +// Copyright 2025 Apple Inc. and the Swift Homomorphic Encryption project authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// The functions in this extension are the default implementation of async methods of HE scheme. +extension HeScheme { + // swiftlint:disable missing_docs + @inlinable + public static func rotateColumnsAsync( + of ciphertext: inout CanonicalCiphertext, + by step: Int, + using evaluationKey: EvaluationKey) async throws + { + try rotateColumns(of: &ciphertext, by: step, using: evaluationKey) + } + + @inlinable + public static func swapRowsAsync( + of ciphertext: inout CanonicalCiphertext, + using evaluationKey: EvaluationKey) async throws + { + try swapRows(of: &ciphertext, using: evaluationKey) + } + + @inlinable + public static func addAssignAsync(_ lhs: inout CoeffPlaintext, _ rhs: CoeffPlaintext) async throws { + try addAssign(&lhs, rhs) + } + + @inlinable + public static func addAssignAsync(_ lhs: inout EvalPlaintext, _ rhs: EvalPlaintext) async throws { + try addAssign(&lhs, rhs) + } + + @inlinable + public static func addAssignCoeffAsync(_ lhs: inout CoeffCiphertext, _ rhs: CoeffCiphertext) async throws { + try addAssignCoeff(&lhs, rhs) + } + + @inlinable + public static func addAssignEvalAsync(_ lhs: inout EvalCiphertext, _ rhs: EvalCiphertext) async throws { + try addAssignEval(&lhs, rhs) + } + + @inlinable + public static func subAssignCoeffAsync(_ lhs: inout CoeffCiphertext, _ rhs: CoeffCiphertext) async throws { + try subAssignCoeff(&lhs, rhs) + } + + @inlinable + public static func subAssignEvalAsync(_ lhs: inout EvalCiphertext, _ rhs: EvalCiphertext) async throws { + try subAssignEval(&lhs, rhs) + } + + @inlinable + public static func addAssignCoeffAsync( + _ ciphertext: inout CoeffCiphertext, + _ plaintext: CoeffPlaintext) async throws + { + try addAssignCoeff(&ciphertext, plaintext) + } + + @inlinable + public static func addAssignEvalAsync(_ ciphertext: inout EvalCiphertext, _ plaintext: EvalPlaintext) async throws { + try addAssignEval(&ciphertext, plaintext) + } + + @inlinable + public static func subAssignCoeffAsync( + _ ciphertext: inout CoeffCiphertext, + _ plaintext: CoeffPlaintext) async throws + { + try subAssignCoeff(&ciphertext, plaintext) + } + + @inlinable + public static func subAssignEvalAsync(_ ciphertext: inout EvalCiphertext, _ plaintext: EvalPlaintext) async throws { + try subAssignEval(&ciphertext, plaintext) + } + + @inlinable + public static func subCoeffAsync(_ plaintext: CoeffPlaintext, + _ ciphertext: CoeffCiphertext) async throws -> CoeffCiphertext + { + try subCoeff(plaintext, ciphertext) + } + + @inlinable + public static func subEvalAsync(_ plaintext: EvalPlaintext, + _ ciphertext: EvalCiphertext) async throws -> EvalCiphertext + { + try subEval(plaintext, ciphertext) + } + + @inlinable + public static func mulAssignAsync(_ ciphertext: inout EvalCiphertext, _ plaintext: EvalPlaintext) async throws { + try mulAssign(&ciphertext, plaintext) + } + + @inlinable + public static func negAssignCoeffAsync(_ ciphertext: inout CoeffCiphertext) async { + negAssign(&ciphertext) + } + + @inlinable + public static func negAssignEvalAsync(_ ciphertext: inout EvalCiphertext) async { + negAssign(&ciphertext) + } + + @inlinable + public static func innerProductAsync( + _ lhs: some Collection, + _ rhs: some Collection) async throws + -> CanonicalCiphertext + { + try innerProduct(lhs, rhs) + } + + @inlinable + public static func innerProductAsync(ciphertexts: some Collection, + plaintexts: some Collection) async throws -> EvalCiphertext + { + try innerProduct(ciphertexts: ciphertexts, plaintexts: plaintexts) + } + + @inlinable + public static func innerProductAsync(ciphertexts: some Collection, + plaintexts: some Collection) async throws -> EvalCiphertext + { + try innerProduct(ciphertexts: ciphertexts, plaintexts: plaintexts) + } + + @inlinable + public static func mulAssignAsync(_ lhs: inout CanonicalCiphertext, _ rhs: CanonicalCiphertext) async throws { + try mulAssign(&lhs, rhs) + } + + @inlinable + public static func addAssignAsync(_ lhs: inout CanonicalCiphertext, _ rhs: CanonicalCiphertext) async throws { + try addAssign(&lhs, rhs) + } + + @inlinable + public static func subAssignAsync(_ lhs: inout CanonicalCiphertext, _ rhs: CanonicalCiphertext) async throws { + try subAssign(&lhs, rhs) + } + + @inlinable + public static func subAssignAsync( + _ ciphertext: inout CanonicalCiphertext, + _ plaintext: CoeffPlaintext) async throws + { + try subAssign(&ciphertext, plaintext) + } + + @inlinable + public static func subAssignAsync(_ ciphertext: inout CanonicalCiphertext, + _ plaintext: EvalPlaintext) async throws + { + try subAssign(&ciphertext, plaintext) + } + + @inlinable + public static func subAsync(_ plaintext: CoeffPlaintext, + _ ciphertext: CanonicalCiphertext) async throws -> CanonicalCiphertext + { + try sub(plaintext, ciphertext) + } + + @inlinable + public static func subAsync(_ plaintext: EvalPlaintext, + _ ciphertext: CanonicalCiphertext) async throws -> CanonicalCiphertext + { + try sub(plaintext, ciphertext) + } + + @inlinable + public static func modSwitchDownAsync(_ ciphertext: inout CanonicalCiphertext) async throws { + try modSwitchDown(&ciphertext) + } + + @inlinable + public static func applyGaloisAsync( + ciphertext: inout CanonicalCiphertext, + element: Int, + using key: EvaluationKey) async throws + { + try applyGalois(ciphertext: &ciphertext, element: element, using: key) + } + + @inlinable + public static func relinearizeAsync(_ ciphertext: inout CanonicalCiphertext, + using key: EvaluationKey) async throws + { + try relinearize(&ciphertext, using: key) + } + + @inlinable + public static func forwardNttAsync(_ ciphertext: CoeffCiphertext) async throws -> EvalCiphertext { + try forwardNtt(ciphertext) + } + + @inlinable + public static func inverseNttAsync(_ ciphertext: EvalCiphertext) async throws -> CoeffCiphertext { + try inverseNtt(ciphertext) + } + + @inlinable + public static func modSwitchDownToSingleAsync(_ ciphertext: inout CanonicalCiphertext) async throws { + try modSwitchDownToSingle(&ciphertext) + } + + @inlinable + public static func multiplyInversePowerOfXAsync(_ ciphertext: inout CoeffCiphertext, power: Int) async throws { + try multiplyInversePowerOfX(&ciphertext, power: power) + } + // swiftlint:enable missing_docs +} diff --git a/Sources/HomomorphicEncryption/NoOpScheme.swift b/Sources/HomomorphicEncryption/NoOpScheme.swift index 334fd996..6e7f9b00 100644 --- a/Sources/HomomorphicEncryption/NoOpScheme.swift +++ b/Sources/HomomorphicEncryption/NoOpScheme.swift @@ -1,4 +1,4 @@ -// Copyright 2024 Apple Inc. and the Swift Homomorphic Encryption project authors +// Copyright 2024-2025 Apple Inc. and the Swift Homomorphic Encryption project authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -302,4 +302,20 @@ public enum NoOpScheme: HeScheme { { minNoiseBudget } + + public static func forwardNtt(_ ciphertext: CoeffCiphertext) throws -> EvalCiphertext { + let polys = try ciphertext.polys.map { try $0.forwardNtt() } + return Ciphertext(context: ciphertext.context, + polys: polys, + correctionFactor: ciphertext.correctionFactor, + seed: ciphertext.seed) + } + + public static func inverseNtt(_ ciphertext: EvalCiphertext) throws -> CoeffCiphertext { + let polys = try ciphertext.polys.map { try $0.inverseNtt() } + return Ciphertext(context: ciphertext.context, + polys: polys, + correctionFactor: ciphertext.correctionFactor, + seed: ciphertext.seed) + } } diff --git a/Sources/PNNSGenerateDatabase/PNNSGenerateDatabase.docc/PNNSGenerateDatabase.md b/Sources/PNNSGenerateDatabase/PNNSGenerateDatabase.docc/PNNSGenerateDatabase.md index 6b8e2762..d41dacd5 100644 --- a/Sources/PNNSGenerateDatabase/PNNSGenerateDatabase.docc/PNNSGenerateDatabase.md +++ b/Sources/PNNSGenerateDatabase/PNNSGenerateDatabase.docc/PNNSGenerateDatabase.md @@ -8,7 +8,7 @@ Private Nearest Neighbor Search database generation The resulting database can be processed with the [PNNSProcessDatabase](https://swiftpackageindex.com/apple/swift-homomorphic-encryption/main/documentation/pnnsprocessdatabase) executable. ### Requirements -First ensure sure that the `~/.swiftpm/bin` directory is on your `$PATH`. +First ensure that the `~/.swiftpm/bin` directory is on your `$PATH`. For example, if using the `zsh` shell, add the following line to your `~/.zshrc` ```sh export PATH="$HOME/.swiftpm/bin:$PATH" diff --git a/Sources/PNNSProcessDatabase/PNNSProcessDatabase.docc/PNNSProcessDatabase.md b/Sources/PNNSProcessDatabase/PNNSProcessDatabase.docc/PNNSProcessDatabase.md index 79620d0d..239553da 100644 --- a/Sources/PNNSProcessDatabase/PNNSProcessDatabase.docc/PNNSProcessDatabase.md +++ b/Sources/PNNSProcessDatabase/PNNSProcessDatabase.docc/PNNSProcessDatabase.md @@ -7,7 +7,7 @@ PNNS database processing will transform a database in preparation for hosting PN The `PNNSProcessDatabase` binary performs the processing. ### Requirements -First ensure sure that the `~/.swiftpm/bin` directory is on your `$PATH`. +First ensure that the `~/.swiftpm/bin` directory is on your `$PATH`. For example, if using the `zsh` shell, add the following line to your `~/.zshrc` ```sh export PATH="$HOME/.swiftpm/bin:$PATH" diff --git a/Sources/TestUtilities/HeApiTestUtils.swift b/Sources/TestUtilities/HeApiTestUtils.swift index e7d4834e..3f3199de 100644 --- a/Sources/TestUtilities/HeApiTestUtils.swift +++ b/Sources/TestUtilities/HeApiTestUtils.swift @@ -1,4 +1,4 @@ -// Copyright 2025 Apple Inc. and the Swift Homomorphic Encryption project authors +// Copyright 2024-2025 Apple Inc. and the Swift Homomorphic Encryption project authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -296,7 +296,7 @@ public enum HeAPITestHelpers { } /// Testing ciphertext addition of the scheme. - public static func schemeCiphertextAddTest(context: Context) throws { + public static func schemeCiphertextAddTest(context: Context) async throws { let testEnv = try TestEnv(context: context, format: .coefficient) let data1 = testEnv.data1 let data2 = testEnv.data2 @@ -322,6 +322,9 @@ public enum HeAPITestHelpers { var sum = canonicalCipher1 try sum += canonicalCipher2 try testEnv.checkDecryptsDecodes(ciphertext: sum, format: .coefficient, expected: sumData) + var sumAsync = canonicalCipher1 + try await Scheme.addAssignAsync(&sumAsync, canonicalCipher2) + try testEnv.checkDecryptsDecodes(ciphertext: sumAsync, format: .coefficient, expected: sumData) } // canonicalCiphertext + coeffCiphertext @@ -367,6 +370,10 @@ public enum HeAPITestHelpers { var sum = coeffCipher1 try sum += coeffCipher2 try testEnv.checkDecryptsDecodes(ciphertext: sum, format: .coefficient, expected: sumData) + + var sumAsync = coeffCipher1 + try await Scheme.addAssignCoeffAsync(&sumAsync, coeffCipher2) + try testEnv.checkDecryptsDecodes(ciphertext: sumAsync, format: .coefficient, expected: sumData) } } @@ -383,12 +390,16 @@ public enum HeAPITestHelpers { var sum = evalCipher1 try sum += evalCipher2 try testEnv.checkDecryptsDecodes(ciphertext: sum, format: .coefficient, expected: sumData) + + var sumAsync = evalCipher1 + try await Scheme.addAssignEvalAsync(&sumAsync, evalCipher2) + try testEnv.checkDecryptsDecodes(ciphertext: sumAsync, format: .coefficient, expected: sumData) } } } /// Testing ciphertext subtraction of the scheme. - public static func schemeCiphertextSubtractTest(context: Context) throws { + public static func schemeCiphertextSubtractTest(context: Context) async throws { let testEnv = try TestEnv(context: context, format: .coefficient) let data1 = testEnv.data1 let data2 = testEnv.data2 @@ -414,6 +425,10 @@ public enum HeAPITestHelpers { var diff = canonicalCipher1 try diff -= canonicalCipher2 try testEnv.checkDecryptsDecodes(ciphertext: diff, format: .coefficient, expected: diffData) + + var diffAsync = canonicalCipher1 + try await Scheme.subAssignAsync(&diffAsync, canonicalCipher2) + try testEnv.checkDecryptsDecodes(ciphertext: diffAsync, format: .coefficient, expected: diffData) } // canonicalCiphertext - coeffCiphertext @@ -459,6 +474,10 @@ public enum HeAPITestHelpers { var diff = coeffCipher1 try diff -= coeffCipher2 try testEnv.checkDecryptsDecodes(ciphertext: diff, format: .coefficient, expected: diffData) + + var diffAsync = coeffCipher1 + try await Scheme.subAssignCoeffAsync(&diffAsync, coeffCipher2) + try testEnv.checkDecryptsDecodes(ciphertext: diffAsync, format: .coefficient, expected: diffData) } } @@ -475,12 +494,18 @@ public enum HeAPITestHelpers { var diff = evalCipher1 try diff -= evalCipher2 try testEnv.checkDecryptsDecodes(ciphertext: diff, format: .coefficient, expected: diffData) + + var diffAsync = evalCipher1 + try await Scheme.subAssignEvalAsync(&diffAsync, evalCipher2) + try testEnv.checkDecryptsDecodes(ciphertext: diffAsync, format: .coefficient, expected: diffData) } } } /// testing ciphertext multiplication of the scheme. - public static func schemeCiphertextCiphertextMultiplyTest(context: Context) throws { + public static func schemeCiphertextCiphertextMultiplyTest( + context: Context) async throws + { guard context.supportsSimdEncoding, context.supportsEvaluationKey else { return } @@ -493,9 +518,14 @@ public enum HeAPITestHelpers { let ciphertext1 = testEnv.ciphertext1 let ciphertext2 = testEnv.ciphertext2 let ciphertextProduct = try ciphertext1 * ciphertext2 + var ciphertextProductAsync = ciphertext1 + try await Scheme.mulAssignAsync(&ciphertextProductAsync, ciphertext2) var relinearizedProd = ciphertextProduct try relinearizedProd.relinearize(using: XCTUnwrap(testEnv.evaluationKey)) + var relinearizedProdAsync = ciphertextProductAsync + try await Scheme.relinearizeAsync(&relinearizedProdAsync, using: XCTUnwrap(testEnv.evaluationKey)) XCTAssertEqual(relinearizedProd.polys.count, Scheme.freshCiphertextPolyCount) + XCTAssertEqual(relinearizedProdAsync.polys.count, Scheme.freshCiphertextPolyCount) let evalCiphertext: Ciphertext = try ciphertextProduct.convertToEvalFormat() let coeffCiphertext: Ciphertext = try evalCiphertext.inverseNtt() @@ -505,12 +535,15 @@ public enum HeAPITestHelpers { try testEnv.checkDecryptsDecodes(ciphertext: coeffCiphertext, format: .simd, expected: productData) try testEnv.checkDecryptsDecodes(ciphertext: evalCiphertext, format: .simd, expected: productData) try testEnv.checkDecryptsDecodes(ciphertext: ciphertextProduct, format: .simd, expected: productData) + try testEnv.checkDecryptsDecodes(ciphertext: ciphertextProductAsync, format: .simd, expected: productData) try testEnv.checkDecryptsDecodes(ciphertext: coeffRelinearizedCiphertext, format: .simd, expected: productData) try testEnv.checkDecryptsDecodes(ciphertext: evalRelinearizedCiphertext, format: .simd, expected: productData) } /// Testing CT-PT inner product of the scheme. - public static func schemeCiphertextPlaintextInnerProductTest(context: Context) throws { + public static func schemeCiphertextPlaintextInnerProductTest( + context: Context) async throws + { let testEnv = try TestEnv(context: context, format: .simd) let data1 = testEnv.data1 let data2 = testEnv.data2 @@ -527,6 +560,14 @@ public enum HeAPITestHelpers { 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) } // no nil values do { @@ -538,6 +579,14 @@ public enum HeAPITestHelpers { count: count) let innerProduct = try XCTUnwrap(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) } // some nil values do { @@ -549,12 +598,22 @@ public enum HeAPITestHelpers { count: count) + [nil] let innerProduct = try XCTUnwrap(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) } } } /// Testing CT-CT inner product of the scheme. - public static func schemeCiphertextCiphertextInnerProductTest(context: Context) throws { + public static func schemeCiphertextCiphertextInnerProductTest( + context: Context) async throws + { let testEnv = try TestEnv(context: context, format: .simd) let data1 = testEnv.data1 let data2 = testEnv.data2 @@ -568,34 +627,14 @@ 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) - } - } - - /// Testing CT-CT inner product with nil of the scheme. - public static func schemeCiphertextCiphertextNilInnerProductTest( - context: Context) throws - { - let testEnv = try TestEnv(context: context, format: .simd) - let data1 = testEnv.data1 - let data2 = testEnv.data2 - for count in [4, 257] { - let innerProductData = zip(data1, data2) - .map { x, y in - let t = context.plaintextModulus - let xTimesY = x.multiplyMod(y, modulus: t, variableTime: true) - return xTimesY.multiplyMod(Scheme.Scalar(count), modulus: t, variableTime: true) - } - let innerProduct = try Array(repeating: testEnv.ciphertext1, count: count).innerProduct(ciphertexts: Array( - repeating: testEnv.ciphertext2, - count: count)) - - try testEnv.checkDecryptsDecodes(ciphertext: innerProduct, format: .simd, expected: innerProductData) + try testEnv.checkDecryptsDecodes(ciphertext: innerProductAsync, format: .simd, expected: innerProductData) } } /// Testing CT-CT multiplication followed by CT-CT addition of the scheme. - public static func schemeCiphertextMultiplyAddTest(context: Context) throws { + public static func schemeCiphertextMultiplyAddTest(context: Context) async throws { guard context.supportsSimdEncoding else { return } @@ -611,12 +650,17 @@ public enum HeAPITestHelpers { let ciphertext2 = testEnv.ciphertext2 let ciphertextResult = try ciphertext1 * ciphertext2 + ciphertext1 + var ciphertextResultAsync = ciphertext1 + try await Scheme.mulAssignAsync(&ciphertextResultAsync, ciphertext2) + try await Scheme.addAssignAsync(&ciphertextResultAsync, ciphertext1) + let evalCiphertext: Ciphertext = try ciphertextResult.convertToEvalFormat() let coeffCiphertext: Ciphertext = try evalCiphertext.inverseNtt() try testEnv.checkDecryptsDecodes(ciphertext: coeffCiphertext, format: .simd, expected: multiplyAddData) try testEnv.checkDecryptsDecodes(ciphertext: evalCiphertext, format: .simd, expected: multiplyAddData) try testEnv.checkDecryptsDecodes(ciphertext: ciphertextResult, format: .simd, expected: multiplyAddData) + try testEnv.checkDecryptsDecodes(ciphertext: ciphertextResultAsync, format: .simd, expected: multiplyAddData) } /// Testing CT-CT multiplication followed by CT-PT addition of the scheme. @@ -644,34 +688,9 @@ public enum HeAPITestHelpers { try testEnv.checkDecryptsDecodes(ciphertext: ciphertextResult, format: .simd, expected: multiplyAddData) } - /// Testing CT-CT multiplication followed by CT-CT subtraction of the scheme. - public static func schemeCiphertextMultiplySubtractTest(context: Context) throws { - guard context.supportsSimdEncoding else { - return - } - let testEnv = try TestEnv(context: context, format: .simd) - let data1 = testEnv.data1 - let data2 = testEnv.data2 - let multiplySubtractData = zip(data1, data2).map { data1, data2 in - let t = context.plaintextModulus - return data1.multiplyMod(data2, modulus: t, variableTime: true).subtractMod(data1, modulus: t) - } - - let ciphertext1 = testEnv.ciphertext1 - let ciphertext2 = testEnv.ciphertext2 - let ciphertextResult = try ciphertext1 * ciphertext2 - ciphertext1 - - let evalCiphertext: Ciphertext = try ciphertextResult.convertToEvalFormat() - let coeffCiphertext: Ciphertext = try evalCiphertext.inverseNtt() - - try testEnv.checkDecryptsDecodes(ciphertext: coeffCiphertext, format: .simd, expected: multiplySubtractData) - try testEnv.checkDecryptsDecodes(ciphertext: evalCiphertext, format: .simd, expected: multiplySubtractData) - try testEnv.checkDecryptsDecodes(ciphertext: ciphertextResult, format: .simd, expected: multiplySubtractData) - } - /// Testing CT-CT multiplication followed by CT-PT subtraction of the scheme. public static func schemeCiphertextMultiplySubtractPlainTest( - context: Context) throws + context: Context) async throws { guard context.supportsSimdEncoding else { return @@ -688,17 +707,25 @@ public enum HeAPITestHelpers { let ciphertext2 = testEnv.ciphertext2 let ciphertextResult = try ciphertext1 * ciphertext2 - testEnv.coeffPlaintext1 + var ciphertextResultAsync = ciphertext1 + try await Scheme.mulAssignAsync(&ciphertextResultAsync, ciphertext2) + try await Scheme.subAssignAsync(&ciphertextResultAsync, testEnv.coeffPlaintext1) + let evalCiphertext: Ciphertext = try ciphertextResult.convertToEvalFormat() let coeffCiphertext: Ciphertext = try evalCiphertext.inverseNtt() try testEnv.checkDecryptsDecodes(ciphertext: coeffCiphertext, format: .simd, expected: multiplySubtractData) try testEnv.checkDecryptsDecodes(ciphertext: evalCiphertext, format: .simd, expected: multiplySubtractData) try testEnv.checkDecryptsDecodes(ciphertext: ciphertextResult, format: .simd, expected: multiplySubtractData) + try testEnv.checkDecryptsDecodes( + ciphertext: ciphertextResultAsync, + format: .simd, + expected: multiplySubtractData) } /// Testing CT-PT multiplication followed by CT-PT addition of the scheme. public static func schemeCiphertextPlaintextMultiplyAddPlainTest( - context: Context) throws + context: Context) async throws { guard context.supportsSimdEncoding else { return @@ -716,17 +743,23 @@ public enum HeAPITestHelpers { var ciphertextResult = try ciphertextEvalResult.inverseNtt() try ciphertextResult += testEnv.coeffPlaintext1 + var ciphertextEvalResultAsync = ciphertext1 + try await Scheme.mulAssignAsync(&ciphertextEvalResultAsync, testEnv.evalPlaintext2) + var ciphertextResultAsync = try await Scheme.inverseNttAsync(ciphertextEvalResultAsync) + try await Scheme.addAssignCoeffAsync(&ciphertextResultAsync, testEnv.coeffPlaintext1) + let evalCiphertext: Ciphertext = try ciphertextResult.convertToEvalFormat() let coeffCiphertext: Ciphertext = try evalCiphertext.inverseNtt() try testEnv.checkDecryptsDecodes(ciphertext: coeffCiphertext, format: .simd, expected: multiplyAddData) try testEnv.checkDecryptsDecodes(ciphertext: evalCiphertext, format: .simd, expected: multiplyAddData) try testEnv.checkDecryptsDecodes(ciphertext: ciphertextResult, format: .simd, expected: multiplyAddData) + try testEnv.checkDecryptsDecodes(ciphertext: ciphertextResultAsync, format: .simd, expected: multiplyAddData) } /// Testing CT-PT multiplication followed by CT-PT subtraction of the scheme. public static func schemeCiphertextPlaintextMultiplySubtractPlainTest( - context: Context) throws + context: Context) async throws { guard context.supportsSimdEncoding else { return @@ -744,16 +777,58 @@ public enum HeAPITestHelpers { var ciphertextResult = try ciphertextEvalResult.inverseNtt() try ciphertextResult -= testEnv.coeffPlaintext1 + var ciphertextEvalResultAsync = ciphertext1 + try await Scheme.mulAssignAsync(&ciphertextEvalResultAsync, testEnv.evalPlaintext2) + var ciphertextResultAsync = try await Scheme.inverseNttAsync(ciphertextEvalResultAsync) + try await Scheme.subAssignCoeffAsync(&ciphertextResultAsync, testEnv.coeffPlaintext1) + let evalCiphertext: Ciphertext = try ciphertextResult.convertToEvalFormat() let coeffCiphertext: Ciphertext = try evalCiphertext.inverseNtt() try testEnv.checkDecryptsDecodes(ciphertext: coeffCiphertext, format: .simd, expected: multiplySubtractData) try testEnv.checkDecryptsDecodes(ciphertext: evalCiphertext, format: .simd, expected: multiplySubtractData) try testEnv.checkDecryptsDecodes(ciphertext: ciphertextResult, format: .simd, expected: multiplySubtractData) + try testEnv.checkDecryptsDecodes( + ciphertext: ciphertextResultAsync, + format: .simd, + expected: multiplySubtractData) + } + + /// Testing CT-CT multiplication followed by CT-CT subtraction of the scheme. + public static func schemeCiphertextMultiplySubtractTest(context: Context) async throws { + guard context.supportsSimdEncoding else { + return + } + let testEnv = try TestEnv(context: context, format: .simd) + let data1 = testEnv.data1 + let data2 = testEnv.data2 + let multiplySubtractData = zip(data1, data2).map { data1, data2 in + let t = context.plaintextModulus + return data1.multiplyMod(data2, modulus: t, variableTime: true).subtractMod(data1, modulus: t) + } + + let ciphertext1 = testEnv.ciphertext1 + let ciphertext2 = testEnv.ciphertext2 + let ciphertextResult = try ciphertext1 * ciphertext2 - ciphertext1 + + var ciphertextResultAsync = ciphertext1 + try await Scheme.mulAssignAsync(&ciphertextResultAsync, ciphertext2) + try await Scheme.subAssignAsync(&ciphertextResultAsync, ciphertext1) + + let evalCiphertext: Ciphertext = try ciphertextResult.convertToEvalFormat() + let coeffCiphertext: Ciphertext = try evalCiphertext.inverseNtt() + + try testEnv.checkDecryptsDecodes(ciphertext: coeffCiphertext, format: .simd, expected: multiplySubtractData) + try testEnv.checkDecryptsDecodes(ciphertext: evalCiphertext, format: .simd, expected: multiplySubtractData) + try testEnv.checkDecryptsDecodes(ciphertext: ciphertextResult, format: .simd, expected: multiplySubtractData) + try testEnv.checkDecryptsDecodes( + ciphertext: ciphertextResultAsync, + format: .simd, + expected: multiplySubtractData) } /// Testing ciphertext negation of the scheme. - public static func schemeCiphertextNegateTest(context: Context) throws { + public static func schemeCiphertextNegateTest(context: Context) async throws { let testEnv = try TestEnv(context: context, format: .coefficient) let negatedData = testEnv.data1.map { data1 in data1.negateMod(modulus: context.plaintextModulus) @@ -761,17 +836,25 @@ public enum HeAPITestHelpers { let ciphertextResult = -testEnv.ciphertext1 let evalCiphertext = -testEnv.evalCiphertext1 + var evalCiphertextAsync = testEnv.evalCiphertext1 + await Scheme.negAssignEvalAsync(&evalCiphertextAsync) var coeffCiphertext: Ciphertext = try testEnv.evalCiphertext1.inverseNtt() + + var coeffCiphertextAsync = coeffCiphertext + await Scheme.negAssignCoeffAsync(&coeffCiphertextAsync) + coeffCiphertext = -coeffCiphertext try testEnv.checkDecryptsDecodes(ciphertext: coeffCiphertext, format: .coefficient, expected: negatedData) try testEnv.checkDecryptsDecodes(ciphertext: evalCiphertext, format: .coefficient, expected: negatedData) + try testEnv.checkDecryptsDecodes(ciphertext: coeffCiphertextAsync, format: .coefficient, expected: negatedData) + try testEnv.checkDecryptsDecodes(ciphertext: evalCiphertextAsync, format: .coefficient, expected: negatedData) try testEnv.checkDecryptsDecodes(ciphertext: ciphertextResult, format: .coefficient, expected: negatedData) } /// Testing CT-PT addition of the scheme. - public static func schemeCiphertextPlaintextAddTest(context: Context) throws { + public static func schemeCiphertextPlaintextAddTest(context: Context) async throws { guard context.supportsSimdEncoding else { return } @@ -849,6 +932,10 @@ public enum HeAPITestHelpers { var sum = coeffCiphertext try sum += coeffPlaintext try testEnv.checkDecryptsDecodes(ciphertext: sum, format: .simd, expected: sumData) + + var sumAsync = coeffCiphertext + try await Scheme.addAssignCoeffAsync(&sumAsync, coeffPlaintext) + try testEnv.checkDecryptsDecodes(ciphertext: sumAsync, format: .simd, expected: sumData) } } @@ -875,12 +962,18 @@ public enum HeAPITestHelpers { var sum = evalCiphertext try sum += evalPlaintext try testEnv.checkDecryptsDecodes(ciphertext: sum, format: .simd, expected: sumData) + + var sumAsync = evalCiphertext + try await Scheme.addAssignEvalAsync(&sumAsync, evalPlaintext) + try testEnv.checkDecryptsDecodes(ciphertext: sumAsync, format: .simd, expected: sumData) } catch HeError.unsupportedHeOperation(_) {} } } /// Testing CT-PT subtraction of the scheme. - public static func schemeCiphertextPlaintextSubtractTest(context: Context) throws { + public static func schemeCiphertextPlaintextSubtractTest( + context: Context) async throws + { guard context.supportsSimdEncoding else { return } @@ -953,6 +1046,10 @@ public enum HeAPITestHelpers { var diff = coeffCiphertext try diff -= coeffPlaintext try testEnv.checkDecryptsDecodes(ciphertext: diff, format: .simd, expected: diff1Minus2Data) + + var diffAsync = coeffCiphertext + try await Scheme.subAssignCoeffAsync(&diffAsync, coeffPlaintext) + try testEnv.checkDecryptsDecodes(ciphertext: diffAsync, format: .simd, expected: diff1Minus2Data) } } @@ -971,6 +1068,10 @@ public enum HeAPITestHelpers { var diff = evalCiphertext try diff -= evalPlaintext try testEnv.checkDecryptsDecodes(ciphertext: diff, format: .simd, expected: diff1Minus2Data) + + var diffAsync = evalCiphertext + try await Scheme.subAssignEvalAsync(&diffAsync, evalPlaintext) + try testEnv.checkDecryptsDecodes(ciphertext: diffAsync, format: .simd, expected: diff1Minus2Data) } catch HeError.unsupportedHeOperation(_) {} // evalPlaintext - evalCiphertext @@ -984,7 +1085,9 @@ public enum HeAPITestHelpers { } /// Testing CT-PT multiplication of the scheme. - public static func schemeCiphertextPlaintextMultiplyTest(context: Context) throws { + public static func schemeCiphertextPlaintextMultiplyTest( + context: Context) async throws + { guard context.supportsSimdEncoding else { return } @@ -1002,8 +1105,12 @@ public enum HeAPITestHelpers { let ciphertext = testEnv.evalCiphertext1 let evalPlaintext = testEnv.evalPlaintext2 + var productAsync = ciphertext + try await Scheme.mulAssignAsync(&productAsync, evalPlaintext) // cipher * plain try testEnv.checkDecryptsDecodes(ciphertext: ciphertext * evalPlaintext, format: .simd, expected: productData) + + try testEnv.checkDecryptsDecodes(ciphertext: productAsync, format: .simd, expected: productData) // with mod-switch down if context.coefficientModuli.count > 2 { var ciphertext = testEnv.ciphertext1 @@ -1017,6 +1124,15 @@ public enum HeAPITestHelpers { ciphertext: evalCiphertext * evalPlaintext, format: .simd, expected: productData) + + var ciphertextAsync = testEnv.ciphertext1 + try await Scheme.modSwitchDownAsync(&ciphertextAsync) + var evalCiphertextAsync = try ciphertextAsync.convertToEvalFormat() + try await Scheme.mulAssignAsync(&evalCiphertextAsync, evalPlaintext) + try testEnv.checkDecryptsDecodes( + ciphertext: evalCiphertextAsync, + format: .simd, + expected: productData) } // plain * cipher try testEnv.checkDecryptsDecodes( @@ -1026,8 +1142,8 @@ public enum HeAPITestHelpers { } /// Testign ciphertext rotation of the scheme. - public static func schemeRotationTest(context: Context) throws { - func runRotationTest(context: Context, galoisElements: [Int], multiStep: Bool) throws { + public static func schemeRotationTest(context: Context) async throws { + func runRotationTest(context: Context, galoisElements: [Int], multiStep: Bool) async throws { let degree = context.degree let testEnv = try TestEnv(context: context, format: .simd, galoisElements: galoisElements) let evaluationKey = try XCTUnwrap(testEnv.evaluationKey) @@ -1036,19 +1152,38 @@ public enum HeAPITestHelpers { .data1[0..> 1)).flatMap { step in try [ @@ -1076,12 +1218,12 @@ public enum HeAPITestHelpers { } let galoisElementsMultiStep = try GaloisElement.rotatingColumnsMultiStep(degree: degree) - try runRotationTest(context: context, galoisElements: galoisElementsRotate, multiStep: false) - try runRotationTest(context: context, galoisElements: galoisElementsMultiStep, multiStep: true) + try await runRotationTest(context: context, galoisElements: galoisElementsRotate, multiStep: false) + try await runRotationTest(context: context, galoisElements: galoisElementsMultiStep, multiStep: true) } /// Testing apply Galois element of the scheme. - public static func schemeApplyGaloisTest(context: Context) throws { + public static func schemeApplyGaloisTest(context: Context) async throws { guard context.supportsSimdEncoding, context.supportsEvaluationKey else { return } @@ -1101,13 +1243,122 @@ public enum HeAPITestHelpers { for (step, element) in elements.enumerated() { for modSwitchCount in 0...max(0, context.coefficientModuli.count - 2) { var rotatedCiphertext = testEnv.ciphertext1 + var rotatedCiphertextAsync = testEnv.ciphertext1 + for _ in 0..(context: Context) throws { + let testEnv = try TestEnv(context: context, format: .coefficient) + + let zeroCoeffCiphertext = try Scheme.CoeffCiphertext.zero(context: context, moduliCount: 1) + XCTAssertEqual( + try zeroCoeffCiphertext.noiseBudget(using: testEnv.secretKey, variableTime: true), + Double.infinity) + let zeroEvalCiphertext = try Scheme.EvalCiphertext.zero(context: context, moduliCount: 1) + XCTAssertEqual( + try zeroEvalCiphertext.noiseBudget(using: testEnv.secretKey, variableTime: true), + Double.infinity) + + var coeffCiphertext = testEnv.ciphertext1 + var expected = testEnv.coeffPlaintext1 + try coeffCiphertext.modSwitchDownToSingle() + var ciphertext = try coeffCiphertext.convertToEvalFormat() + + var noiseBudget = try ciphertext.noiseBudget(using: testEnv.secretKey, variableTime: true) + XCTAssert(noiseBudget > 0) + + let coeffNoiseBudget = try ciphertext.convertToCoeffFormat().noiseBudget( + using: testEnv.secretKey, + variableTime: true) + let canonicalNoiseBudget = try ciphertext.convertToCanonicalFormat().noiseBudget( + using: testEnv.secretKey, + variableTime: true) + XCTAssertEqual(coeffNoiseBudget, noiseBudget) + XCTAssertEqual(canonicalNoiseBudget, noiseBudget) + + while noiseBudget > Scheme.minNoiseBudget + 1 { + ciphertext = try ciphertext + ciphertext + try expected += expected + let newNoiseBudget = try ciphertext.noiseBudget(using: testEnv.secretKey, variableTime: true) + XCTAssertIsClose(newNoiseBudget, noiseBudget - 1) + noiseBudget = newNoiseBudget + + let decrypted = try ciphertext.decrypt(using: testEnv.secretKey) + XCTAssertEqual(decrypted, expected) + } + // Two more additions yields incorrect results + ciphertext = try ciphertext + ciphertext + ciphertext = try ciphertext + ciphertext + try expected += expected + try expected += expected + let decrypted = try ciphertext.decrypt(using: testEnv.secretKey) + XCTAssertNotEqual(decrypted, expected) + } + + /// testing repeated addition. + public static func repeatAdditionTest(context: Context) async throws { + let testEnv = try HeAPITestHelpers.TestEnv(context: context, format: .coefficient) + + var coeffCiphertext = testEnv.ciphertext1 + var coeffCiphertextAsync = try coeffCiphertext.convertToCoeffFormat() + let coeffCifertextToAdd = try coeffCiphertext.convertToCoeffFormat() + try coeffCiphertext += testEnv.coeffPlaintext1 + try coeffCiphertext += testEnv.ciphertext1 + try coeffCiphertext += testEnv.coeffPlaintext1 + try coeffCiphertext += testEnv.ciphertext1 + + try await Scheme.addAssignCoeffAsync(&coeffCiphertextAsync, testEnv.coeffPlaintext1) + try await Scheme.addAssignCoeffAsync(&coeffCiphertextAsync, coeffCifertextToAdd) + try await Scheme.addAssignCoeffAsync(&coeffCiphertextAsync, testEnv.coeffPlaintext1) + try await Scheme.addAssignCoeffAsync(&coeffCiphertextAsync, coeffCifertextToAdd) + + let expected = testEnv.data1.map { num in + num.multiplyMod(Scheme.Scalar(5), modulus: testEnv.context.plaintextModulus, variableTime: true) + } + try testEnv.checkDecryptsDecodes(ciphertext: coeffCiphertext, format: .coefficient, expected: expected) + try testEnv.checkDecryptsDecodes(ciphertext: coeffCiphertextAsync, format: .coefficient, expected: expected) + } + + /// testing multiply inverse power of x. + public static func multiplyInverseTest(context: Context) async throws { + let testEnv = try HeAPITestHelpers.TestEnv(context: context, format: .coefficient) + + var coeffCiphertext1 = try testEnv.ciphertext1.convertToCoeffFormat() + var coeffCiphertext2 = coeffCiphertext1 + var coeffCiphertext3 = coeffCiphertext1 + var coeffCiphertext4 = coeffCiphertext1 + let degree = context.degree + let plaintextModulus = context.plaintextModulus + let power1 = Int.random(in: 0..(context: Context>) throws { @@ -129,54 +129,6 @@ class HeAPITests: XCTestCase { XCTAssertEqual(decrypted, testEnv.data1) } - private func bfvNoiseBudgetTest(context: Context>) throws { - let testEnv = try TestEnv(context: context, format: .coefficient) - - let zeroCoeffCiphertext = try Bfv.CoeffCiphertext.zero(context: context, moduliCount: 1) - XCTAssertEqual( - try zeroCoeffCiphertext.noiseBudget(using: testEnv.secretKey, variableTime: true), - Double.infinity) - let zeroEvalCiphertext = try Bfv.EvalCiphertext.zero(context: context, moduliCount: 1) - XCTAssertEqual( - try zeroEvalCiphertext.noiseBudget(using: testEnv.secretKey, variableTime: true), - Double.infinity) - - var coeffCiphertext = testEnv.ciphertext1 - var expected = testEnv.coeffPlaintext1 - try coeffCiphertext.modSwitchDownToSingle() - var ciphertext = try coeffCiphertext.convertToEvalFormat() - - var noiseBudget = try ciphertext.noiseBudget(using: testEnv.secretKey, variableTime: true) - XCTAssert(noiseBudget > 0) - - let coeffNoiseBudget = try ciphertext.convertToCoeffFormat().noiseBudget( - using: testEnv.secretKey, - variableTime: true) - let canonicalNoiseBudget = try ciphertext.convertToCanonicalFormat().noiseBudget( - using: testEnv.secretKey, - variableTime: true) - XCTAssertEqual(coeffNoiseBudget, noiseBudget) - XCTAssertEqual(canonicalNoiseBudget, noiseBudget) - - while noiseBudget > Bfv.minNoiseBudget + 1 { - ciphertext = try ciphertext + ciphertext - try expected += expected - let newNoiseBudget = try ciphertext.noiseBudget(using: testEnv.secretKey, variableTime: true) - XCTAssertIsClose(newNoiseBudget, noiseBudget - 1) - noiseBudget = newNoiseBudget - - let decrypted = try ciphertext.decrypt(using: testEnv.secretKey) - XCTAssertEqual(decrypted, expected) - } - // Two more additions yields incorrect results - ciphertext = try ciphertext + ciphertext - ciphertext = try ciphertext + ciphertext - try expected += expected - try expected += expected - let decrypted = try ciphertext.decrypt(using: testEnv.secretKey) - XCTAssertNotEqual(decrypted, expected) - } - private func runBfvTests(_: T.Type) async throws { let predefined: [EncryptionParameters>] = try PredefinedRlweParameters.allCases .filter { rlweParams in rlweParams.supportsScalar(T.self) } @@ -202,24 +154,26 @@ class HeAPITests: XCTestCase { try HeAPITestHelpers.schemeEncryptZeroDecryptTest(context: context) try HeAPITestHelpers.schemeEncryptZeroAddDecryptTest(context: context) try HeAPITestHelpers.schemeEncryptZeroMultiplyDecryptTest(context: context) - try HeAPITestHelpers.schemeCiphertextAddTest(context: context) - try HeAPITestHelpers.schemeCiphertextSubtractTest(context: context) - try HeAPITestHelpers.schemeCiphertextCiphertextMultiplyTest(context: context) - try HeAPITestHelpers.schemeCiphertextPlaintextAddTest(context: context) - try HeAPITestHelpers.schemeCiphertextPlaintextSubtractTest(context: context) - try HeAPITestHelpers.schemeCiphertextPlaintextMultiplyTest(context: context) - try HeAPITestHelpers.schemeCiphertextMultiplyAddTest(context: context) + try await HeAPITestHelpers.schemeCiphertextAddTest(context: context) + try await HeAPITestHelpers.schemeCiphertextSubtractTest(context: context) + try await HeAPITestHelpers.schemeCiphertextCiphertextMultiplyTest(context: context) + try await HeAPITestHelpers.schemeCiphertextPlaintextAddTest(context: context) + try await HeAPITestHelpers.schemeCiphertextPlaintextSubtractTest(context: context) + try await HeAPITestHelpers.schemeCiphertextPlaintextMultiplyTest(context: context) + try await HeAPITestHelpers.schemeCiphertextMultiplyAddTest(context: context) try HeAPITestHelpers.schemeCiphertextMultiplyAddPlainTest(context: context) - try HeAPITestHelpers.schemeCiphertextMultiplySubtractTest(context: context) - try HeAPITestHelpers.schemeCiphertextMultiplySubtractPlainTest(context: context) - try HeAPITestHelpers.schemeCiphertextNegateTest(context: context) - try HeAPITestHelpers.schemeCiphertextPlaintextInnerProductTest(context: context) - try HeAPITestHelpers.schemeCiphertextCiphertextInnerProductTest(context: context) + try await HeAPITestHelpers.schemeCiphertextMultiplySubtractTest(context: context) + try await HeAPITestHelpers.schemeCiphertextMultiplySubtractPlainTest(context: context) + try await HeAPITestHelpers.schemeCiphertextNegateTest(context: context) + try await HeAPITestHelpers.schemeCiphertextPlaintextInnerProductTest(context: context) + try await HeAPITestHelpers.schemeCiphertextCiphertextInnerProductTest(context: context) try HeAPITestHelpers.schemeEvaluationKeyTest(context: context) - try HeAPITestHelpers.schemeRotationTest(context: context) - try HeAPITestHelpers.schemeApplyGaloisTest(context: context) + try await HeAPITestHelpers.schemeRotationTest(context: context) + try await HeAPITestHelpers.schemeApplyGaloisTest(context: context) try bfvTestKeySwitching(context: context) - try bfvNoiseBudgetTest(context: context) + try HeAPITestHelpers.noiseBudgetTest(context: context) + try await HeAPITestHelpers.repeatAdditionTest(context: context) + try await HeAPITestHelpers.multiplyInverseTest(context: context) } }