Skip to content

Commit 3b68c75

Browse files
fboemerfeimaomiao
authored andcommitted
Use encodingEntrySize for IndexPir (apple#267)
* Use encodingEntrySize for IndexPir
1 parent 34a152e commit 3b68c75

File tree

14 files changed

+264
-78
lines changed

14 files changed

+264
-78
lines changed

Sources/ApplicationProtobuf/PirConversion.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,8 @@ extension Apple_SwiftHomomorphicEncryption_Pir_V1_PirParameters {
175175
entrySizeInBytes: Int(entrySize),
176176
dimensions: dimensions.map(Int.init),
177177
batchSize: Int(batchSize),
178-
evaluationKeyConfig: evaluationKeyConfig.native())
178+
evaluationKeyConfig: evaluationKeyConfig.native(),
179+
encodingEntrySize: encodingEntrySize)
179180
}
180181

181182
/// Converts the protobuf object into a native type.

Sources/ApplicationProtobuf/PirConversionApi.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024-2025 Apple Inc. and the Swift Homomorphic Encryption project authors
1+
// Copyright 2024-2026 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.
@@ -50,14 +50,18 @@ extension Apple_SwiftHomomorphicEncryption_Api_Pir_V1_PIRShardConfig {
5050
/// - Parameters:
5151
/// - batchSize: Number of queries in a batch.
5252
/// - evaluationKeyConfig: Evaluation key configuration
53+
/// - encodingEntrySize: Whether to encode the size.
5354
/// - Returns: The converted native type.
54-
public func native(batchSize: Int, evaluationKeyConfig: EvaluationKeyConfig) -> IndexPirParameter {
55+
public func native(batchSize: Int, evaluationKeyConfig: EvaluationKeyConfig, encodingEntrySize: Bool)
56+
-> IndexPirParameter
57+
{
5558
IndexPirParameter(
5659
entryCount: Int(numEntries),
5760
entrySizeInBytes: Int(entrySize),
5861
dimensions: dimensions.map(Int.init),
5962
batchSize: batchSize,
60-
evaluationKeyConfig: evaluationKeyConfig)
63+
evaluationKeyConfig: evaluationKeyConfig,
64+
encodingEntrySize: encodingEntrySize)
6165
}
6266
}
6367

Sources/ApplicationProtobuf/generated/apple_swift_homomorphic_encryption_pir_v1_pir.pb.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// For information on using the generated types, please see the documentation:
99
// https://github.com/apple/swift-protobuf/
1010

11-
// Copyright 2024-2025 Apple Inc. and the Swift Homomorphic Encryption project authors
11+
// Copyright 2024-2026 Apple Inc. and the Swift Homomorphic Encryption project authors
1212
//
1313
// Licensed under the Apache License, Version 2.0 (the "License");
1414
// you may not use this file except in compliance with the License.
@@ -196,6 +196,12 @@ public struct Apple_SwiftHomomorphicEncryption_Pir_V1_PirParameters: @unchecked
196196
set {_uniqueStorage()._keyCompressionStrategy = newValue}
197197
}
198198

199+
/// Whether to encode the entry size as part of the Index PIR response.
200+
public var encodingEntrySize: Bool {
201+
get {return _storage._encodingEntrySize}
202+
set {_uniqueStorage()._encodingEntrySize = newValue}
203+
}
204+
199205
public var unknownFields = SwiftProtobuf.UnknownStorage()
200206

201207
public init() {}
@@ -358,7 +364,7 @@ extension Apple_SwiftHomomorphicEncryption_Pir_V1_SymmetricPirConfigType: SwiftP
358364

359365
extension Apple_SwiftHomomorphicEncryption_Pir_V1_PirParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
360366
public static let protoMessageName: String = _protobuf_package + ".PirParameters"
361-
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}encryption_parameters\0\u{3}num_entries\0\u{3}entry_size\0\u{1}dimensions\0\u{3}keyword_pir_params\0\u{1}algorithm\0\u{3}batch_size\0\u{3}evaluation_key_config\0\u{3}key_compression_strategy\0\u{c}\u{a}\u{1}\u{c}\u{b}\u{1}\u{c}\u{c}\u{1}")
367+
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}encryption_parameters\0\u{3}num_entries\0\u{3}entry_size\0\u{1}dimensions\0\u{3}keyword_pir_params\0\u{1}algorithm\0\u{3}batch_size\0\u{3}evaluation_key_config\0\u{3}key_compression_strategy\0\u{4}\u{4}encoding_entry_size\0\u{c}\u{a}\u{1}\u{c}\u{b}\u{1}\u{c}\u{c}\u{1}")
362368

363369
fileprivate class _StorageClass {
364370
var _encryptionParameters: HomomorphicEncryptionProtobuf.Apple_SwiftHomomorphicEncryption_V1_EncryptionParameters? = nil
@@ -370,6 +376,7 @@ extension Apple_SwiftHomomorphicEncryption_Pir_V1_PirParameters: SwiftProtobuf.M
370376
var _batchSize: UInt64 = 0
371377
var _evaluationKeyConfig: HomomorphicEncryptionProtobuf.Apple_SwiftHomomorphicEncryption_V1_EvaluationKeyConfig? = nil
372378
var _keyCompressionStrategy: Apple_SwiftHomomorphicEncryption_Pir_V1_KeyCompressionStrategy = .unspecified
379+
var _encodingEntrySize: Bool = false
373380

374381
// This property is used as the initial default value for new instances of the type.
375382
// The type itself is protecting the reference to its storage via CoW semantics.
@@ -389,6 +396,7 @@ extension Apple_SwiftHomomorphicEncryption_Pir_V1_PirParameters: SwiftProtobuf.M
389396
_batchSize = source._batchSize
390397
_evaluationKeyConfig = source._evaluationKeyConfig
391398
_keyCompressionStrategy = source._keyCompressionStrategy
399+
_encodingEntrySize = source._encodingEntrySize
392400
}
393401
}
394402

@@ -416,6 +424,7 @@ extension Apple_SwiftHomomorphicEncryption_Pir_V1_PirParameters: SwiftProtobuf.M
416424
case 7: try { try decoder.decodeSingularUInt64Field(value: &_storage._batchSize) }()
417425
case 8: try { try decoder.decodeSingularMessageField(value: &_storage._evaluationKeyConfig) }()
418426
case 9: try { try decoder.decodeSingularEnumField(value: &_storage._keyCompressionStrategy) }()
427+
case 13: try { try decoder.decodeSingularBoolField(value: &_storage._encodingEntrySize) }()
419428
default: break
420429
}
421430
}
@@ -455,6 +464,9 @@ extension Apple_SwiftHomomorphicEncryption_Pir_V1_PirParameters: SwiftProtobuf.M
455464
if _storage._keyCompressionStrategy != .unspecified {
456465
try visitor.visitSingularEnumField(value: _storage._keyCompressionStrategy, fieldNumber: 9)
457466
}
467+
if _storage._encodingEntrySize != false {
468+
try visitor.visitSingularBoolField(value: _storage._encodingEntrySize, fieldNumber: 13)
469+
}
458470
}
459471
try unknownFields.traverse(visitor: &visitor)
460472
}
@@ -473,6 +485,7 @@ extension Apple_SwiftHomomorphicEncryption_Pir_V1_PirParameters: SwiftProtobuf.M
473485
if _storage._batchSize != rhs_storage._batchSize {return false}
474486
if _storage._evaluationKeyConfig != rhs_storage._evaluationKeyConfig {return false}
475487
if _storage._keyCompressionStrategy != rhs_storage._keyCompressionStrategy {return false}
488+
if _storage._encodingEntrySize != rhs_storage._encodingEntrySize {return false}
476489
return true
477490
}
478491
if !storagesAreEqual {return false}

Sources/PrivateInformationRetrieval/IndexPir/IndexPirProtocol.swift

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ public enum PirKeyCompressionStrategy: String, CaseIterable, Codable, CodingKeyR
4343
/// Configuration for an Index PIR database.
4444
public struct IndexPirConfig: Hashable, Codable, Sendable {
4545
/// Number of entries in the database.
46-
public let entryCount: Int
47-
/// Byte size of each entry in the database.
48-
public let entrySizeInBytes: Int
46+
public var entryCount: Int
47+
/// Byte size of the largest entry in the database.
48+
public var entrySizeInBytes: Int
4949
/// Number of dimensions in the database.
5050
public let dimensionCount: Int
5151
/// Number of indices in a query to the database.
@@ -54,23 +54,38 @@ public struct IndexPirConfig: Hashable, Codable, Sendable {
5454
public let unevenDimensions: Bool
5555
/// Evaluation key compression.
5656
public let keyCompression: PirKeyCompressionStrategy
57+
/// Whether to encode the entry size.
58+
public var encodingEntrySize: Bool
59+
60+
/// Size of the largest entry in bytes after encoding.
61+
public var encodedEntrySize: Int {
62+
if encodingEntrySize {
63+
// VarInt is monotonic, i.e. the largest entry will always have the largest encoded entry size.
64+
// So we can take an upper bound here.
65+
VarInt.encodedSize(UInt32(entrySizeInBytes)) + entrySizeInBytes
66+
} else {
67+
entrySizeInBytes
68+
}
69+
}
5770

5871
/// Initializes an ``IndexPirConfig``.
5972
/// - Parameters:
6073
/// - entryCount: Number of entries in the database.
61-
/// - entrySizeInBytes: Byte size of each entry in the database.
74+
/// - entrySizeInBytes: Byte size of the largest entry in the database.
6275
/// - dimensionCount: Number of dimensions in database.
6376
/// - batchSize: Number of indices in a query to the database.
6477
/// - unevenDimensions: Whether or not to enable `uneven dimensions` optimization.
6578
/// - keyCompression: Evaluation key compression.
79+
/// - encodingEntrySize: Whether or not to encode each entry's size.
6680
/// - Throws: Error upon invalid configuration parameters.
6781
public init(
6882
entryCount: Int,
6983
entrySizeInBytes: Int,
7084
dimensionCount: Int,
7185
batchSize: Int,
7286
unevenDimensions: Bool,
73-
keyCompression: PirKeyCompressionStrategy) throws
87+
keyCompression: PirKeyCompressionStrategy,
88+
encodingEntrySize: Bool) throws
7489
{
7590
let validDimensionsCount = [1, 2]
7691
guard validDimensionsCount.contains(dimensionCount) else {
@@ -82,6 +97,7 @@ public struct IndexPirConfig: Hashable, Codable, Sendable {
8297
self.batchSize = batchSize
8398
self.unevenDimensions = unevenDimensions
8499
self.keyCompression = keyCompression
100+
self.encodingEntrySize = encodingEntrySize
85101
}
86102
}
87103

@@ -91,14 +107,27 @@ public struct IndexPirConfig: Hashable, Codable, Sendable {
91107
public struct IndexPirParameter: Hashable, Codable, Sendable {
92108
/// Number of entries in the database.
93109
public let entryCount: Int
94-
/// Byte size of each entry in the database.
110+
/// Byte size of the largest entry in the database, excluding any encoding of the entry size.
95111
public let entrySizeInBytes: Int
96112
/// Number of plaintexts in each dimension of the database.
97113
public let dimensions: [Int]
98114
/// Number of indices in a query to the database.
99115
public let batchSize: Int
100116
/// Evaluation key configuration.
101117
public let evaluationKeyConfig: EvaluationKeyConfig
118+
/// Whether to encode the entry size.
119+
public var encodingEntrySize: Bool
120+
121+
/// Size of the largest entry in bytes after encoding.
122+
public var encodedEntrySize: Int {
123+
if encodingEntrySize {
124+
// VarInt is monotonic, i.e. the largest entry will always have the largest encoded entry size.
125+
// So we can take an upper bound here.
126+
VarInt.encodedSize(UInt32(entrySizeInBytes)) + entrySizeInBytes
127+
} else {
128+
entrySizeInBytes
129+
}
130+
}
102131

103132
/// The number of dimensions in the database.
104133
@usableFromInline package var dimensionCount: Int { dimensions.count }
@@ -108,22 +137,25 @@ public struct IndexPirParameter: Hashable, Codable, Sendable {
108137
/// Initializes an ``IndexPirParameter``.
109138
/// - Parameters:
110139
/// - entryCount: Number of entries in the database.
111-
/// - entrySizeInBytes: Byte size of each entry in the database.
140+
/// - entrySizeInBytes: Byte size of the largest entry in the database, without encoding entry size.
112141
/// - dimensions: Number of plaintexts in each dimension of the database.
113142
/// - batchSize: Number of indices in a query to the database.
114143
/// - evaluationKeyConfig: Evaluation key configuration.
144+
/// - encodingEntrySize: Whether to encode the entry size.
115145
public init(
116146
entryCount: Int,
117147
entrySizeInBytes: Int,
118148
dimensions: [Int],
119149
batchSize: Int,
120-
evaluationKeyConfig: EvaluationKeyConfig)
150+
evaluationKeyConfig: EvaluationKeyConfig,
151+
encodingEntrySize: Bool)
121152
{
122153
self.entryCount = entryCount
123154
self.entrySizeInBytes = entrySizeInBytes
124155
self.dimensions = dimensions
125156
self.batchSize = batchSize
126157
self.evaluationKeyConfig = evaluationKeyConfig
158+
self.encodingEntrySize = encodingEntrySize
127159
}
128160
}
129161

Sources/PrivateInformationRetrieval/IndexPir/MulPir.swift

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024-2025 Apple Inc. and the Swift Homomorphic Encryption project authors
1+
// Copyright 2024-2026 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.
@@ -35,9 +35,9 @@ public enum MulPir<Scheme: HeScheme>: IndexPirProtocol {
3535
public static func generateParameter(config: IndexPirConfig,
3636
with context: Scheme.Context) -> IndexPirParameter
3737
{
38-
let entrySizeInBytes = config.entrySizeInBytes
39-
let perChunkPlaintextCount = if entrySizeInBytes <= context.bytesPerPlaintext {
40-
config.entryCount.dividingCeil(context.bytesPerPlaintext / entrySizeInBytes, variableTime: true)
38+
let encodedEntrySize = config.encodedEntrySize
39+
let perChunkPlaintextCount = if encodedEntrySize <= context.bytesPerPlaintext {
40+
config.entryCount.dividingCeil(context.bytesPerPlaintext / encodedEntrySize, variableTime: true)
4141
} else {
4242
config.entryCount
4343
}
@@ -74,9 +74,10 @@ public enum MulPir<Scheme: HeScheme>: IndexPirProtocol {
7474
keyCompression: config.keyCompression)
7575
return IndexPirParameter(
7676
entryCount: config.entryCount,
77-
entrySizeInBytes: entrySizeInBytes,
77+
entrySizeInBytes: config.entrySizeInBytes,
7878
dimensions: dimensions, batchSize: config.batchSize,
79-
evaluationKeyConfig: evalKeyConfig)
79+
evaluationKeyConfig: evalKeyConfig,
80+
encodingEntrySize: config.encodingEntrySize)
8081
}
8182

8283
@inlinable
@@ -134,9 +135,13 @@ public final class MulPirClient<PirUtil: PirUtilProtocol>: IndexPirClient {
134135

135136
@usableFromInline var entrySizeInBytes: Int { parameter.entrySizeInBytes }
136137

138+
@usableFromInline var encodingEntrySize: Bool { parameter.encodingEntrySize }
139+
140+
@usableFromInline var encodedEntrySize: Int { parameter.encodedEntrySize }
141+
137142
@usableFromInline var entryChunksPerPlaintext: Int {
138-
if context.bytesPerPlaintext >= entrySizeInBytes {
139-
return context.bytesPerPlaintext / entrySizeInBytes
143+
if context.bytesPerPlaintext >= encodedEntrySize {
144+
return context.bytesPerPlaintext / encodedEntrySize
140145
}
141146
return 1
142147
}
@@ -214,12 +219,12 @@ extension MulPirClient {
214219

215220
extension MulPirClient {
216221
var expectedResponseCiphertextCount: Int {
217-
entrySizeInBytes.dividingCeil(context.bytesPerPlaintext, variableTime: true)
222+
encodedEntrySize.dividingCeil(context.bytesPerPlaintext, variableTime: true)
218223
}
219224

220225
private func computeResponseRangeInBytes(at index: Int) -> Range<Int> {
221226
let position = index % entryChunksPerPlaintext
222-
return position * entrySizeInBytes..<(position + 1) * entrySizeInBytes
227+
return position * encodedEntrySize..<(position + 1) * encodedEntrySize
223228
}
224229

225230
/// Decrypts an encrypted response.
@@ -248,7 +253,12 @@ extension MulPirClient {
248253
bitsPerCoeff: context.plaintextModulus.log2)
249254
}
250255

251-
return Array(bytes[computeResponseRangeInBytes(at: entryIndex)])
256+
let responseBytes = bytes[computeResponseRangeInBytes(at: entryIndex)]
257+
if encodingEntrySize {
258+
let (entrySize, bytesConsumed): (UInt32, Int) = try VarInt.decode(responseBytes)
259+
return Array(responseBytes[(responseBytes.startIndex + bytesConsumed)...].prefix(Int(entrySize)))
260+
}
261+
return Array(responseBytes)
252262
}
253263
}
254264

@@ -336,7 +346,7 @@ public final class MulPirServer<PirUtil: PirUtilProtocol>: IndexPirServer {
336346

337347
@inlinable
338348
package static func chunkCount(parameter: IndexPirParameter, context: Scheme.Context) -> Int {
339-
parameter.entrySizeInBytes.dividingCeil(context.bytesPerPlaintext, variableTime: true)
349+
parameter.encodedEntrySize.dividingCeil(context.bytesPerPlaintext, variableTime: true)
340350
}
341351
}
342352

@@ -437,12 +447,12 @@ extension MulPirServer {
437447
throw PirError
438448
.invalidDatabaseEntryCount(entryCount: database.count, expected: parameter.entryCount)
439449
}
440-
let maximumElementSize = database.map(\.count).max() ?? 0
441-
guard maximumElementSize <= parameter.entrySizeInBytes else {
450+
let maxEntrySize = database.map(\.count).max() ?? 0
451+
guard maxEntrySize <= parameter.entrySizeInBytes else {
442452
throw PirError
443-
.invalidDatabaseEntrySize(maximumEntrySize: maximumElementSize, expected: parameter.entrySizeInBytes)
453+
.invalidDatabaseEntrySize(maximumEntrySize: maxEntrySize, expected: parameter.entrySizeInBytes)
444454
}
445-
let chunkCount = parameter.entrySizeInBytes.dividingCeil(context.bytesPerPlaintext, variableTime: true)
455+
let chunkCount = parameter.encodedEntrySize.dividingCeil(context.bytesPerPlaintext, variableTime: true)
446456
if chunkCount > 1 {
447457
return try await processSplitLargeEntries(database: database, with: context, using: parameter)
448458
}
@@ -457,14 +467,22 @@ extension MulPirServer {
457467
{
458468
let chunkCount = Self.chunkCount(parameter: parameter, context: context)
459469
var plaintexts: [[Plaintext<Scheme, Eval>?]] = try await .init(database.async.map { entry in
460-
try await .init(stride(from: 0, to: parameter.entrySizeInBytes, by: context.bytesPerPlaintext).async
470+
let encoded = VarInt.encode(UInt32(entry.count))
471+
let entryEncodingSize = if parameter.encodingEntrySize { encoded.count } else { 0 }
472+
return try await .init(stride(from: 0, to: parameter.encodedEntrySize, by: context.bytesPerPlaintext).async
461473
.map { startIndex in
462-
let endIndex = min(startIndex + context.bytesPerPlaintext, entry.count)
474+
let entryStartIndex = startIndex - entryEncodingSize
475+
let endIndex = min(entryStartIndex + context.bytesPerPlaintext, entry.count)
463476
// Avoid computing on padding plaintexts
464-
guard startIndex < endIndex else {
477+
guard entryStartIndex < endIndex else {
465478
return nil
466479
}
467-
let bytes = Array(entry[startIndex..<endIndex])
480+
let bytes = if startIndex == 0, parameter.encodingEntrySize {
481+
encoded + entry[0..<endIndex]
482+
} else {
483+
Array(entry[entryStartIndex..<endIndex])
484+
}
485+
468486
let coefficients: [Scheme.Scalar] = try CoefficientPacking.bytesToCoefficients(
469487
bytes: bytes,
470488
bitsPerCoeff: context.plaintextModulus.log2,
@@ -504,12 +522,16 @@ extension MulPirServer {
504522
assert(database.count == parameter.entryCount)
505523
let flatDatabase: [UInt8] = database.flatMap { entry in
506524
var entry = entry
507-
let pad = parameter.entrySizeInBytes - entry.count
525+
if parameter.encodingEntrySize {
526+
let encoded = VarInt.encode(UInt32(entry.count))
527+
entry = encoded + entry
528+
}
529+
let pad = parameter.encodedEntrySize - entry.count
508530
entry.append(contentsOf: repeatElement(0, count: pad))
509531
return entry
510532
}
511-
let entriesPerPlaintext = context.bytesPerPlaintext / parameter.entrySizeInBytes
512-
let bytesPerPlaintext = entriesPerPlaintext * parameter.entrySizeInBytes
533+
let entriesPerPlaintext = context.bytesPerPlaintext / parameter.encodedEntrySize
534+
let bytesPerPlaintext = entriesPerPlaintext * parameter.encodedEntrySize
513535
let plaintextIndices = stride(from: 0, to: flatDatabase.count, by: bytesPerPlaintext)
514536
var plaintexts: [Plaintext<Scheme, Eval>?] = try await .init(plaintextIndices.async
515537
.map { startIndex in

0 commit comments

Comments
 (0)