Skip to content

Commit b69ef97

Browse files
authored
Allow up to 32 coefficient moduli (#199)
1 parent f9aaac9 commit b69ef97

File tree

11 files changed

+164
-36
lines changed

11 files changed

+164
-36
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ env:
3232
SWIFTFORMAT_VERSION: 0.54.0
3333
jobs:
3434
swift-tests:
35-
timeout-minutes: 15
35+
timeout-minutes: 30
3636
runs-on: ubuntu-latest
3737
strategy:
3838
fail-fast: false

Benchmarks/RlweBenchmark/RlweBenchmark.swift

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 Apple Inc. and the Swift Homomorphic Encryption project authors
1+
// Copyright 2024-2025 Apple Inc. and the Swift Homomorphic Encryption project authors
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -115,6 +115,59 @@ enum StaticRlweBenchmarkContext {
115115
}
116116
}
117117

118+
struct EncryptionParametersConfig {
119+
let polyDegree: Int
120+
let plaintextModulusBits: [Int]
121+
let coefficientModulusBits: [Int]
122+
}
123+
124+
extension EncryptionParametersConfig: CustomStringConvertible {
125+
var description: String {
126+
"N=\(polyDegree)/logt=\(plaintextModulusBits)/logq=\(coefficientModulusBits.description)"
127+
}
128+
}
129+
130+
extension EncryptionParameters {
131+
init(config: EncryptionParametersConfig) throws {
132+
let plaintextModuli = try Scheme.Scalar.generatePrimes(
133+
significantBitCounts: config.plaintextModulusBits,
134+
preferringSmall: true,
135+
nttDegree: config.polyDegree)
136+
let coefficientModuli = try Scheme.Scalar.generatePrimes(
137+
significantBitCounts: config.coefficientModulusBits,
138+
preferringSmall: false,
139+
nttDegree: config.polyDegree)
140+
self = try EncryptionParameters<Scheme>(
141+
polyDegree: config.polyDegree,
142+
plaintextModulus: plaintextModuli[0],
143+
coefficientModuli: coefficientModuli,
144+
errorStdDev: ErrorStdDev.stdDev32,
145+
securityLevel: SecurityLevel.quantum128)
146+
}
147+
}
148+
149+
let contextCreationConfig32 = EncryptionParametersConfig(
150+
polyDegree: 8192,
151+
plaintextModulusBits: [18],
152+
coefficientModulusBits: Array(repeating: 28, count: 5))
153+
let contextCreationConfig64 = EncryptionParametersConfig(
154+
polyDegree: 8192,
155+
plaintextModulusBits: [18],
156+
coefficientModulusBits: Array(repeating: 33, count: 5))
157+
158+
func contextInitBenchmark<Scheme: HeScheme>(_: Scheme.Type, config: EncryptionParametersConfig) -> () -> Void {
159+
{
160+
let benchmarkName = ["ContextInit", String(describing: Scheme.self), config.description].joined(separator: "/")
161+
Benchmark(benchmarkName, configuration: benchmarkConfiguration) { benchmark in
162+
let encryptionParameters = try EncryptionParameters<Scheme>(config: config)
163+
benchmark.startMeasurement()
164+
for _ in benchmark.scaledIterations {
165+
try blackHole(_ = Context(encryptionParameters: encryptionParameters))
166+
}
167+
}
168+
}
169+
}
170+
118171
func encodeCoefficientBenchmark<Scheme: HeScheme>(_: Scheme.Type) -> () -> Void {
119172
{
120173
benchmark("EncodeCoefficient", Scheme.self) { benchmark in
@@ -235,8 +288,7 @@ func noiseBudgetBenchmark<Scheme: HeScheme>(_: Scheme.Type) -> () -> Void {
235288
benchmark.startMeasurement()
236289
for _ in benchmark.scaledIterations {
237290
try blackHole(
238-
benchmarkContext.ciphertext
239-
.noiseBudget(using: benchmarkContext.secretKey, variableTime: true))
291+
benchmarkContext.ciphertext.noiseBudget(using: benchmarkContext.secretKey, variableTime: true))
240292
}
241293
}
242294
}
@@ -548,6 +600,10 @@ func evaluationKeyDeserializeSeedBenchmark<Scheme: HeScheme>(_: Scheme.Type) ->
548600

549601
// swiftlint:disable:next closure_body_length
550602
nonisolated(unsafe) let benchmarks: () -> Void = {
603+
// Context
604+
contextInitBenchmark(Bfv<UInt32>.self, config: contextCreationConfig32)()
605+
contextInitBenchmark(Bfv<UInt64>.self, config: contextCreationConfig64)()
606+
551607
// Encode/decode
552608
encodeSimdBenchmark(Bfv<UInt32>.self)()
553609
encodeSimdBenchmark(Bfv<UInt64>.self)()

Sources/HomomorphicEncryption/Bfv/Bfv+Decrypt.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 Apple Inc. and the Swift Homomorphic Encryption project authors
1+
// Copyright 2024-2025 Apple Inc. and the Swift Homomorphic Encryption project authors
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -82,12 +82,18 @@ extension Bfv {
8282
case tMax..<pow(tMax, 2):
8383
precondition(variableTime)
8484
return try computeNoiseBudget(of: vTimesT, T.DoubleWidth.self)
85-
case tMax..<pow(tMax, 4):
85+
case pow(tMax, 2)..<pow(tMax, 4):
8686
precondition(variableTime)
8787
return try computeNoiseBudget(of: vTimesT, QuadWidth<T>.self)
88-
case tMax..<pow(tMax, 8):
88+
case pow(tMax, 4)..<pow(tMax, 8):
8989
precondition(variableTime)
9090
return try computeNoiseBudget(of: vTimesT, OctoWidth<T>.self)
91+
case pow(tMax, 8)..<pow(tMax, 16):
92+
precondition(variableTime)
93+
return try computeNoiseBudget(of: vTimesT, Width16<T>.self)
94+
case pow(tMax, 16)..<pow(tMax, 32):
95+
precondition(variableTime)
96+
return try computeNoiseBudget(of: vTimesT, Width32<T>.self)
9197
default:
9298
preconditionFailure("crtMaxIntermediateValue \(crtMaxIntermediateValue) too large")
9399
}

Sources/HomomorphicEncryption/DoubleWidthUInt.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 Apple Inc. and the Swift Homomorphic Encryption project authors
1+
// Copyright 2024-2025 Apple Inc. and the Swift Homomorphic Encryption project authors
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -307,6 +307,7 @@ extension DoubleWidthUInt: FixedWidthInteger {
307307
return (result, didCarry)
308308
}
309309

310+
@inlinable
310311
public func quotientAndRemainder(
311312
dividingBy other: DoubleWidthUInt) -> (quotient: DoubleWidthUInt, remainder: DoubleWidthUInt)
312313
{
@@ -720,3 +721,5 @@ extension DoubleWidthUInt: UnsignedInteger where Base: FixedWidthInteger & Unsig
720721
@usableFromInline typealias DWUInt128 = DoubleWidthUInt<UInt64>
721722
@usableFromInline typealias QuadWidth<T: FixedWidthInteger & UnsignedInteger> = DoubleWidthUInt<DoubleWidthUInt<T>>
722723
@usableFromInline typealias OctoWidth<T: FixedWidthInteger & UnsignedInteger> = DoubleWidthUInt<QuadWidth<T>>
724+
@usableFromInline typealias Width16<T: FixedWidthInteger & UnsignedInteger> = DoubleWidthUInt<OctoWidth<T>>
725+
@usableFromInline typealias Width32<T: FixedWidthInteger & UnsignedInteger> = DoubleWidthUInt<Width16<T>>

Sources/HomomorphicEncryption/EncryptionParameters.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 Apple Inc. and the Swift Homomorphic Encryption project authors
1+
// Copyright 2024-2025 Apple Inc. and the Swift Homomorphic Encryption project authors
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -136,8 +136,8 @@ public struct EncryptionParameters<Scheme: HeScheme>: Hashable, Codable, Sendabl
136136
else {
137137
throw HeError.insecureEncryptionParameters(self)
138138
}
139-
// Due to some usage of OctoWidth
140-
guard coefficientModuli.count <= 8 else {
139+
// Due to some usage of `Width32`
140+
guard coefficientModuli.count <= 32 else {
141141
throw HeError.invalidEncryptionParameters(self)
142142
}
143143
for coefficientModulus in coefficientModuli {

Sources/HomomorphicEncryption/PolyRq/PolyContext.swift

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 Apple Inc. and the Swift Homomorphic Encryption project authors
1+
// Copyright 2024-2025 Apple Inc. and the Swift Homomorphic Encryption project authors
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -19,6 +19,8 @@ public final class PolyContext<T: ScalarType>: Sendable {
1919
public let degree: Int
2020
/// CRT-representation of the modulus `Q = product_{i=0}^{L-1} q_i`.
2121
public let moduli: [T]
22+
/// The modulus `Q = product_{i=0}^{L-1} q_i`.
23+
@usableFromInline let modulus: Width32<T>
2224
/// Next context, typically formed by dropping `q_{L-1}`.
2325
@usableFromInline let next: PolyContext<T>?
2426
/// Operations mod `q_0` up to `q_{L-1}`.
@@ -41,11 +43,12 @@ public final class PolyContext<T: ScalarType>: Sendable {
4143
guard degree.isPowerOfTwo else {
4244
throw HeError.invalidDegree(degree)
4345
}
44-
// For CRT correctness, we require all moduli to be co-prime.
45-
// For convenience, we instead check a slightly stronger condition, that
46-
// additionally restricts an even modulus to be a power of two. This unnecessarily forbids,
47-
// 6, e.g., from being in the RNS base.
48-
for modulus in moduli {
46+
47+
func validate(modulus: T) throws {
48+
// For CRT correctness, we require all moduli to be co-prime.
49+
// For convenience, we instead check a slightly stronger condition, that
50+
// additionally restricts an even modulus to be a power of two. This unnecessarily forbids,
51+
// 6, e.g., from being in the RNS base.
4952
guard modulus.isPrime(variableTime: true) || modulus.isPowerOfTwo else {
5053
throw HeError.invalidModulus(Int64(modulus))
5154
}
@@ -62,6 +65,19 @@ public final class PolyContext<T: ScalarType>: Sendable {
6265
guard moduli.allUnique() else {
6366
throw HeError.coprimeModuli(moduli: moduli.map { Int64($0) })
6467
}
68+
guard let lastModulus = moduli.last else {
69+
throw HeError.emptyModulus
70+
}
71+
if let next {
72+
precondition(next.moduli[...] == moduli.prefix(moduli.count - 1))
73+
try validate(modulus: lastModulus)
74+
self.modulus = next.modulus &* Width32<T>(lastModulus)
75+
} else {
76+
for modulus in moduli {
77+
try validate(modulus: modulus)
78+
}
79+
self.modulus = moduli.product()
80+
}
6581

6682
self.degree = degree
6783
self.moduli = moduli

Sources/HomomorphicEncryption/RnsTool.swift

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 Apple Inc. and the Swift Homomorphic Encryption project authors
1+
// Copyright 2024-2025 Apple Inc. and the Swift Homomorphic Encryption project authors
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -108,15 +108,13 @@ package struct RnsTool<T: ScalarType>: Sendable {
108108
self.qModT = inputContext.qRemainder(dividingBy: t)
109109
self.tIncrement = inputContext.moduli.map { qi in qi - t.modulus }
110110
self.t = t
111+
let composedQ = inputContext.modulus
112+
let composedT = outputContext.modulus
113+
let qDivTComposed = composedQ / composedT
111114

112-
// At least 8 moduli supported, more when their product is far from `T.max`.
113-
let octoModuli = inputContext.moduli.map { modulus in OctoWidth<T>(integerLiteral: Int(modulus)) }
114-
let q: OctoWidth<T> = inputContext.moduli.product()
115-
let qDivT = q / OctoWidth<T>(integerLiteral: Int(t.modulus))
116-
117-
self.qDivT = octoModuli.map { qi in MultiplyConstantModulus(
118-
multiplicand: T(qDivT % qi),
119-
modulus: T(qi),
115+
self.qDivT = inputContext.moduli.map { qi in MultiplyConstantModulus(
116+
multiplicand: T(qDivTComposed % Width32<T>(qi)),
117+
modulus: qi,
120118
variableTime: true)
121119
}
122120

Sources/HomomorphicEncryption/Util.swift

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 Apple Inc. and the Swift Homomorphic Encryption project authors
1+
// Copyright 2024-2025 Apple Inc. and the Swift Homomorphic Encryption project authors
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -73,3 +73,38 @@ extension Array where Element: FixedWidthInteger {
7373
reduce(V(0)) { V($0) + V($1) }
7474
}
7575
}
76+
77+
extension Array where Element: ScalarType {
78+
@inlinable
79+
func product() -> Width32<Self.Element> {
80+
func wideningProduct<Prod: FixedWidthInteger>(of elements: [some FixedWidthInteger]) -> [Prod] {
81+
stride(from: 0, to: elements.count, by: 2).map { index in
82+
var product = Prod(elements[index])
83+
if index < elements.count - 1 {
84+
product &*= Prod(elements[index + 1])
85+
}
86+
return product
87+
}
88+
}
89+
90+
let doubleWidth: [Self.Element.DoubleWidth] = wideningProduct(of: self)
91+
if doubleWidth.count == 1 {
92+
return Width32<Self.Element>(doubleWidth[0])
93+
}
94+
let quadWidth: [QuadWidth<Self.Element>] = wideningProduct(of: doubleWidth)
95+
if quadWidth.count == 1 {
96+
return Width32<Self.Element>(quadWidth[0])
97+
}
98+
let octoWidth: [OctoWidth<Self.Element>] = wideningProduct(of: quadWidth)
99+
if octoWidth.count == 1 {
100+
return Width32<Self.Element>(octoWidth[0])
101+
}
102+
let width16: [Width16<Self.Element>] = wideningProduct(of: octoWidth)
103+
if width16.count == 1 {
104+
return Width32<Self.Element>(width16[0])
105+
}
106+
let width32: [Width32<Self.Element>] = wideningProduct(of: width16)
107+
precondition(width32.count == 1)
108+
return width32[0]
109+
}
110+
}

Tests/HomomorphicEncryptionTests/HeAPITests.swift

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 Apple Inc. and the Swift Homomorphic Encryption project authors
1+
// Copyright 2024-2025 Apple Inc. and the Swift Homomorphic Encryption project authors
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -1066,16 +1066,27 @@ class HeAPITests: XCTestCase {
10661066
}
10671067
let custom = try EncryptionParameters<Bfv<T>>(
10681068
polyDegree: TestUtils.testPolyDegree,
1069-
plaintextModulus: T
1070-
.generatePrimes(
1071-
significantBitCounts: [12],
1072-
preferringSmall: true,
1073-
nttDegree: TestUtils.testPolyDegree)[0],
1069+
plaintextModulus: T.generatePrimes(
1070+
significantBitCounts: [12],
1071+
preferringSmall: true,
1072+
nttDegree: TestUtils.testPolyDegree)[0],
10741073
coefficientModuli: testCoefficientModuli(),
10751074
errorStdDev: ErrorStdDev.stdDev32,
10761075
securityLevel: SecurityLevel.unchecked)
1076+
let manyModuli = try EncryptionParameters<Bfv<T>>(
1077+
polyDegree: TestUtils.testPolyDegree,
1078+
plaintextModulus: T.generatePrimes(
1079+
significantBitCounts: [12],
1080+
preferringSmall: true,
1081+
nttDegree: TestUtils.testPolyDegree)[0],
1082+
coefficientModuli: T.generatePrimes(
1083+
significantBitCounts: Array(repeating: T.bitWidth - 4, count: 32),
1084+
preferringSmall: false,
1085+
nttDegree: TestUtils.testPolyDegree),
1086+
errorStdDev: ErrorStdDev.stdDev32,
1087+
securityLevel: SecurityLevel.unchecked)
10771088

1078-
for encryptionParameters in predefined + [custom] {
1089+
for encryptionParameters in predefined + [custom, manyModuli] {
10791090
let context = try Context<Bfv<T>>(encryptionParameters: encryptionParameters)
10801091
try schemeEncodeDecodeTest(context: context)
10811092
try schemeEncryptDecryptTest(context: context)

Tests/HomomorphicEncryptionTests/PolyRqTests/PolyContextTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 Apple Inc. and the Swift Homomorphic Encryption project authors
1+
// Copyright 2024-2025 Apple Inc. and the Swift Homomorphic Encryption project authors
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -48,6 +48,7 @@ final class PolyContextTests: XCTestCase {
4848
let context1 = try PolyContext<UInt32>(degree: 4, moduli: [2])
4949

5050
XCTAssertEqual(context3.moduli, [2, 3, 5])
51+
XCTAssertEqual(context3.modulus, Width32<UInt32>(30))
5152
XCTAssertEqual(context3.degree, 4)
5253
XCTAssertEqual(context3.next, context2)
5354
if let context3Next = context3.next {

Tests/HomomorphicEncryptionTests/UtilTests.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 Apple Inc. and the Swift Homomorphic Encryption project authors
1+
// Copyright 2024-2025 Apple Inc. and the Swift Homomorphic Encryption project authors
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -51,6 +51,8 @@ class UtilTests: XCTestCase {
5151
XCTAssertEqual([7].product(), 7)
5252
XCTAssertEqual([1, 2, 3].product(), 6)
5353
XCTAssertEqual([UInt8(255), UInt8(2)].product(), UInt16(510))
54+
55+
XCTAssertEqual([UInt32(1 << 17), UInt32(1 << 17)].product(), Width32<UInt32>(1 << 34))
5456
}
5557

5658
func testSum() {

0 commit comments

Comments
 (0)