CKKS
SHIP bootstrapping.
A shallow half bootstrap from a one-limb coefficient ciphertext to a slots-domain ciphertext at raised precision.
For researchers familiar with CKKS bootstrapping · Source guide · 2394fa5
poulpy-ckks implements SHIP (Cheon, Hanrot, Kim and Stehlé, ePrint 2025/784) as a native CKKS operation.
SHIP is a half bootstrap: a one-limb bottom ciphertext, holding real cleartexts with an explicit gap gamma in its coefficients, is refreshed into a slots-domain ciphertext at the raised precision — without ModUp or EvalMod.
The bottom ciphertext is switched to a regularly-spaced sparse secret; each support slot then contributes one exponential factor through encrypted selector masks and hoisted base-B mux blind rotations, and a binary product tree over the h + 1 factors assembles the result.
Its multiplicative depth is log2(h + 1) + 1 products, independent of the bottom modulus.
Public API
Invariant-bearing data is exported from poulpy_ckks::layouts:
ShipPlancarries the validated instance dimensions: the encoding gaplog_gamma, the working precisionlog_delta_workof the omega ciphertexts, the residual output budgetlog_budget_out, the sparse Hamming weighth, the offset window half-widthw, the mux baseB, and the low-digit basethetaabsorbed into the masking. The raised precision is((tree_depth + 2) * log_delta_work + log_budget_out)rounded up to a limb boundary (ShipPlan::raised_k).ShipSecretSpecsamples and validates the regularly-spaced sparse support: thek-th nonzero coefficient sits atk*N/h + delta_kwithdelta_kin[-w, w]and a uniform sign, all indices distinct.ShipKeySetis validated, unprepared key material;ShipKeySet::generatederives the whole bundle from the dense secret and aShipSecretSpec;ShipKeySet::preparereturns the backend-readyShipKeysPrepared.ShipKeyParametersfingerprints the key-defining plan, radix, and complex flag;ShipKeysLayoutselects the mux gadget digit size and the tensor and conjugation key layouts.
Evaluation is exposed through poulpy_ckks::api::CKKSShipOps on Module<BE>.
All outputs are caller-allocated:
ckks_ship_bootstrap_into: real cleartexts in the firstN/2coefficients of the bottom ciphertext.ckks_ship_bootstrap_complex_into:Re(mu)in the firstN/2coefficients andIm(mu)in the lastN/2(the slots-to-coeffs layout); requires keys generated withcomplex, which doubles the mask material (the mux keys are shared between the halves).ckks_ship_bootstrap_tmp_bytesreports the caller-arena bound, validating the ciphertext and key layouts along the way.
The only SHIP-specific backend hook is the coefficient encoding: the input-dependent conversion of the bottom ciphertext’s public residues into pt0 and the rotated pi plaintexts.
A backend opts in by implementing poulpy_ckks::oep::CKKSShipCoeffEncodingImpl; the scheme definition is exported as poulpy_ckks::encoding::ship_coeff_encodings_host, and a CPU backend adopts it wholesale with poulpy-cpu-ref’s impl_ckks_ship_coeff_encoding! macro.
The rest of SHIP composes existing CKKS multiplication, keyswitching, convolution, and DFT APIs.
Construction outline
use poulpy_ckks::{
api::CKKSShipOps,
layouts::{ShipKeySet, ShipKeysLayout, ShipPlan, ShipSecretSpec},
};
let plan = ShipPlan::new(log_n, log_gamma, log_delta_work, log_budget_out,
sparse_hamming_weight, window, mux_base, theta)?;
let spec = ShipSecretSpec::sample(&plan, &mut source_xs);
let key_set = ShipKeySet::generate::<MyBackend, f64>(
&module, &host_module, &plan, base2k, &spec, &sk_dense_host,
&keys_layout, &mut source_xe, &mut source_xa, &mut scratch,
)?;
let keys = key_set.prepare(&module, &mut scratch)?;
let mut output = module.ckks_ciphertext_alloc(base2k, plan.raised_k(base2k.as_usize()).into());
let bytes = CKKSShipOps::<MyBackend, f64>::ckks_ship_bootstrap_tmp_bytes(&module, &output, &input, &keys)?;
// ... allocate scratch of `bytes` ...
CKKSShipOps::<MyBackend, f64>::ckks_ship_bootstrap_into(&module, &mut output, &input, &keys, &mut scratch)?;
Key and ciphertext contract
The bottom input has ring degree N, rank one, the key radix, and torus width exactly one limb (base2k bits).
Its torus content is sum_i round(q0 * mu_i / gamma) X^i — value mu_i / gamma at log_delta = base2k with no budget.
It decrypts under the application’s dense secret; the pipeline switches it to the sparse secret internally through the bundle’s encapsulation key.
The output is under the dense secret, at scale log_delta_work, with at least log_budget_out budget.
The key bundle carries, per support slot, the 4*theta encrypted selector masks (prepared as left convolution operands; a second omega_2 set when generated with complex) and one hoisted mux key group per mixed-radix digit position.
A mux key is a rank-2 -> rank-1 switching key whose input secret is (beta * s, beta) and whose output secret is s(X^{g^-1}), so the output automorphism realizes beta * Rot_rot under s; the input gadget decomposition is shared across a digit position, so the input DFT is computed once per position.
The bundle also carries the dense -> sparse encapsulation key — the only object encrypted under the sparse secret, kept at the bottom modulus — plus the product-tree tensor key and the conjugation key, all under the dense secret.
ShipKeySet::new and operation preflight validate layouts, mask and mux counts, gadget dimensions, and the ShipKeyParameters fingerprint.
They cannot inspect the secrets: ShipKeySet::generate enforces provenance by construction, and custom key material must satisfy the same relationships.
Security and parameter selection
SHIP relies on a non-standard sparse secret distribution: ternary, weight exactly h, with the k-th nonzero confined to a 2w + 1 window around k*N/h.
The paper estimates its security through the ternary-key attack of May (Crypto’21), whose cost grows as (4w)^(0.4*h), and uses w = max(175, N/(2h)) in its experiments; treat that estimate as an explicit deployment assumption.
The long-lived application key stays dense — the sparse secret only ever encrypts the bottom modulus 2^(2*base2k) through the encapsulation key.
The phase embedding imposes the same precision contract as PaCo: residues modulo q0 = 2^base2k must be exact in the working scalar, so the f64 path accepts at most 52 residue bits.
For a cleartext of magnitude |mu|, the leading gap-model error is (2*pi)^2 * |mu|^3 / (6 * gamma^2) before homomorphic noise; log_delta_work should sit far enough above the accumulated CKKS noise (roughly 14 bits over the tree) that the gap model dominates.
The mux chain must cover the window: the mixed-radix bases of ShipPlan::mux_bases multiply to at least (2w + 1) / theta candidates, and raising theta trades mux keyswitches for masking convolutions.
Validate precision and security with application-scale parameters rather than the small test instances.