MPC ↔ MHE Protocol Switching¶
Defined across stdlib/sequre/mpc/env.codon, stdlib/sequre/types/internal.codon, and the multiparty type modules
One of Sequre/Shechi's key features is the ability to switch between Secure Multiparty Computation (MPC) and Multiparty Homomorphic Encryption (MHE) mid-computation. Some operations — comparisons, certain matrix multiplications, inverse, square root — are impractical or impossible in pure HE but straightforward in additive secret sharing. Conversely, many linear-algebra operations are more efficient in HE.
How it works¶
The switching mechanism is built on two low-level Lattiseq distributed protocols:
| Protocol | Direction | Description |
|---|---|---|
| E2S (Encryption-to-Shares) | MHE → MPC | Each party collectively decrypts a ciphertext into additive secret shares |
| S2E (Shares-to-Encryption) | MPC → MHE | Each party re-encrypts its additive share; the ciphertexts are summed to produce a fresh collective ciphertext |
A single round-trip (E2S → compute in MPC → S2E) is what via_mpc performs under the hood. The cost of one switch is estimated as:
This is defined by the MHE_MPC_SWITCH_COST_ESTIMATE constant and used by the cost estimator to decide between pure-MHE and switching paths.
The via_mpc method¶
Every multiparty type exposes via_mpc as the primary user-facing switching API:
# On Ciphertensor
result_ct = ct.via_mpc(mpc, lambda stensor: some_mpc_function(mpc, stensor))
# On MPP
result_mpp = mpp.via_mpc(lambda stensor: some_mpc_function(mpc, stensor))
# On MPA
result_mpa = mpa.via_mpc(lambda stensor: some_mpc_function(mpc, stensor))
# On MPU
result_mpu = mpu.via_mpc(lambda stensor: some_mpc_function(mpc, stensor))
The flow inside via_mpc is:
- Collect — gather encrypted data from all parties (if needed)
- E2S — convert each party's
Ciphertensorto aSharetensor(additive share) - Compute — run the user-supplied lambda on the
Sharetensorusing Beaver-triple MPC - S2E — convert the result
Sharetensorback to aCiphertensor
The Ciphertensor variant additionally supports mirrored mode (all parties hold the same ciphertext) and exclude_parties to skip specific parties.
Underlying conversion methods¶
The actual type conversions are implemented in stdlib/sequre/types/internal.codon:
| Method | Direction | Description |
|---|---|---|
Ciphertensor.to_sharetensor(mpc, ...) |
MHE → MPC | Decrypt via E2S into additive shares |
Sharetensor.to_ciphertensor(mpc) |
MPC → MHE | Re-encrypt via S2E into a collective ciphertext |
MPP.to_sharetensor(mpc) |
MHE → MPC | Convert partitioned encryption to shares |
Sharetensor.to_mpp(mpc, ratios) |
MPC → MHE | Convert shares to partitioned encryption |
MPA.to_sharetensor(mpc) |
MHE → MPC | Convert aggregated encryption to shares |
Sharetensor.to_mpa(mpc) |
MPC → MHE | Convert shares to aggregated encryption |
Automatic switching with AllowMPCSwitch¶
For certain operations — notably Ciphertensor.matmul with a plaintext operand — Sequre includes a cost estimator that automatically chooses between a pure-HE path and a switching path. This is gated by the AllowMPCSwitch context manager:
Inside the context, mpc.default_allow_mpc_switch is set to True. The cost estimator compares four strategies for Ciphertensor @ ndarray:
| Strategy | Label | Description |
|---|---|---|
| Via MPC | Via MPC |
E2S → Beaver matmul → S2E |
| M1 | M1 |
Column-packed HE matmul |
| M2 | M2 |
Row-packed HE matmul |
| M3 | M3 |
Diagonal-packed HE matmul |
The cheapest strategy wins. When AllowMPCSwitch is not active, the "Via MPC" cost is set to infinity and only pure-HE paths are considered.
Tip
Enable DEBUG mode to see the cost breakdown printed at each matmul:
Methods that switch automatically¶
Several built-in operations use via_mpc under the hood without requiring AllowMPCSwitch:
Comparisons¶
MPP and MPA comparisons (>, <) always switch to MPC because HE does not natively support comparison:
mask = mpp > 0.0 # internally: mpp.via_mpc(lambda s: secure_operator.gt(mpc, s, 0.0))
mask = mpa < 1.0 # internally: mpa.via_mpc(lambda s: secure_operator.lt(mpc, s, 1.0))
Layout transformations¶
These Ciphertensor methods use via_mpc to materialize pending layout changes:
actual_transpose(mpc)— when a non-diagonal ciphertensor needs an actual (non-lazy) transposediagonal_contig(mpc)— converting to diagonal-contiguous encoding from ciphertext formget_actual(mpc)/iget_actual(mpc)— resolving pending transposition or diagonality
MPP matrix multiplication¶
Certain MPP matmul cases use switching:
_matmul_case_ntnt— non-transposed × non-transposed with a switching path_matmul_case_tt— transposed × transposed with a switching path
Standard-library functions¶
Higher-level Sequre stdlib routines that rely on switching:
| Function | Location | Uses switching for |
|---|---|---|
| Division | (implicit) | Dividing two encrypted operands (iterative Newton method in MPC) |
sqrt(mpc, x) |
stdlib/sequre/stdlib/builtin.codon |
Square root (iterative Newton method in MPC) |
orthonormalize |
applications/gwas.codon |
Gram-Schmidt orthonormalization |
eigen_decomp |
stdlib/sequre/stdlib/pca.codon |
Eigenvalue decomposition |
Monitoring switches¶
The number of E2S/S2E round-trips is tracked by the statistics module:
Use mpc.stats or the mpc.stats_log() context manager to profile and log switching overhead.
When to use switching¶
| Scenario | Recommendation |
|---|---|
Comparison operators (>, <, ==) |
Automatic — always switches |
| Matrix multiply (ciphertext × plaintext) | Use AllowMPCSwitch and let the cost estimator decide |
| Matrix inverse, square root | Use via_mpc explicitly or call the stdlib inv/sqrt |
| Custom non-linear function | Call via_mpc directly with a lambda |
| All operations are linear (add, mul, rotate) | Stay in pure MHE — no switching needed |
Warning
Each switch incurs E2S + S2E communication rounds. For small tensors the overhead may outweigh the benefit. The cost estimator accounts for this, but when calling via_mpc manually, consider whether the operation truly requires MPC.