Skip to main content

HomOps Reference

Bootstrap

Refresh an exhausted CKKS ciphertext so computation can continue.

BsFHE
RunsServer
Changes shapeNo
Changes scaleYes - toward target (or input scale)
Levels spentRefresh - returns to your declared chain
RotatesYes - slot↔coef transforms
KeysSquare + rotation + dense↔sparse

What it does

Bootstrap refreshes a ciphertext that is at (or near) the bottom of the modulus chain and returns a fresh ciphertext at a high level encrypting the same message, up to a small approximation error. Usable depth after each bootstrap is exactly the depth you declared in full_q_list_precision. The bootstrapping circuit's own rows are overhead and are never available to your computation.
Placement is manual: there is no scheduler that inserts bootstraps for you. Put Bootstrap() where the level budget runs out. You may use it more than once; setup and prep metadata is shared across instances, so additional bootstraps do not repeat that cost.

When to use

Use it when your circuit is deeper than any chain you can reasonably configure, or when depth is unknown or unbounded (long iterative pipelines). Bootstrapping itself is expensive, so prefer a modest chain you refresh over a very long chain that inflates every operation, and only pay for a refresh as often as your usable depth requires.

HomParams for a bootstrapping pipeline

Placing Bootstrap() is enough: the compiler infers that the pipeline needs bootstrapping and enables it. You still choose variant, init rows, and special primes:
params.pyPYTHON · BOOT
from lattica_build.params import HomParams, BootstrappingVariant

LOG_N = 16

hom_params = HomParams(
    n=2 ** LOG_N,
    full_q_list_precision=(  # usable depth after each bootstrap
        (60,),
        (60,),
        (60,),
        (60,),
    ),
    pt_scale=2 ** 30,
    num_special_primes=6,
    num_init_rows=0,  # encrypt at q0 alone
    bootstrapping_variant=BootstrappingVariant.REAL,
)
bootstrapping_variant. REAL (default) runs ModRaise → CoefsToSlots → EvalMod → SlotsToCoefs. SLIM moves SlotsToCoefs to the front. SLIM can make that transform cheaper at the bootstrap, but those primes must be carried through the pipeline before it, so longer work between bootstraps usually favors REAL.
num_init_rows. May be 0 when the pipeline includes Bootstrap: a persistent q0 prime is always present, so encryption can happen at q0 alone.
num_special_primes. Required with HYBRID decomposition (the default). These primes are not part of the level budget, but they count toward total modulus and security.
Then place the operator in the pipeline:
pipeline.pyPYTHON · PIPELINE
from lattica_build.base_classes.hom_pipeline import HomomorphicPipeline
from lattica_build.operators.composite.sequential import SequentialHomOp
from lattica_build.operators import HomMul, HomSquare, Bootstrap

pipeline = HomomorphicPipeline(
    hom=SequentialHomOp(
        HomMul(axis_sum=1, with_modswitch=True),
        HomSquare(),
        Bootstrap(),  # place where the level budget runs out
        HomSquare(),
    ),
    input_shape=(128, 512),
)

Signature

Bootstrap(
    log_n_subring=None,       # default: log2(n), no sparse packing
    target_output_scale=None,  # default: preserve input pt_scale
)
No set_data.

Parameters

ParameterTypeDefaultDescription
log_n_subringint | NoneNoneLog₂ of the subring dimension for sparse packing. Default is log2(n) (full ring, no sparse packing). See Sparse packing.
target_output_scaleint | NoneNonePlaintext scale of the bootstrap output. When unset, equals the input plaintext scale (scale-preserving).
Rules that depend on the value
SettingRuleIf you break it
log_n_subringA promise about how the ciphertext was packed. If the repeated structure is missing, the result is silently wrong (blocks are averaged). Pass it only when you control the encoding.No error: incorrect average of blocks.
pt_scaleTarget log2(pt_scale) <= 45. Bootstrapping is designed around an EvalMod scale of 45 with margin against q0; scales above 2⁴⁵ erode that margin and degrade precision. Inputs below 2⁴⁵ are scaled up before EvalMod.Precision degrades as the margin shrinks.

Sparse packing (log_n_subring)

When you need fewer slots than the full ring, encode in a subring of dimension n' = 2**log_n_subring < n and embed it into the full ring (sparse packing). To pack v values you need at least v slots, so choose the smallest power of two with n'/2 >= v. Example: 100 values → 128 slots → n' = 256log_n_subring = 8.
Prepare the input by padding with zeros to fill all n'/2 subring slots, then concatenate that block with itself k = n / n' times to fill all n/2 ring slots before encode/encrypt.
The operator cannot inspect encrypted content: a mismatched log_n_subring fails silently by averaging whatever blocks it finds. Benefit: CoefsToSlots / SlotsToCoefs run over n'/2 slots and rotation-key sets shrink, usually a clear net win despite a small subsum cost.

Requirements

  • HomParams. Placing Bootstrap() is enough; the compiler enables bootstrapping. Set variant / init-row / special-prime as needed.
  • Square key. Polynomial / multiply stages inside the bootstrap need relinearization.
  • Rotation keys. Coefficient↔slot transforms (and sparse-packing subsum rotations) need offsets in the evaluation key.
  • Dense↔sparse switching keys. Two additional switching keys are generated with the pipeline; you do not configure the sparse key's Hamming weight.

Shape effect - no

The output shape equals the input shape. The n axis is unchanged.

Scale effect - yes

Rule: the output scale is target_output_scale when set; otherwise it matches the input plaintext scale. Inputs below 2⁴⁵ may be scaled up before EvalMod.
StageShapeScaleLevels
Before Bootstrap(N,)SNearly exhausted
After Bootstrap(target_output_scale=S')(N,)S' (or S if unset)Top of your full_q_list_precision

Level budget

Bootstrap returns the ciphertext to the top of the chain you declared in full_q_list_precision. Boot circuit rows are added implicitly and consumed by bootstrapping itself; they are not levels you spend with ordinary operators.
StepLevels
Your declared chainUsable depth after each bootstrap = len(full_q_list_precision) path you configured
Bootstrapping circuit12 rows / 531 bits (prepended overhead, not yours to spend)
How primes are organized in the chain is explained on Level budget - The chain.

Modulus & security cost

When the pipeline uses bootstrapping, these contributions add to total modulus (and therefore security sizing at fixed n):
ContributionRowsBits
CoefsToSlots3126
EvalMod (cosine + double-angle)7315
SlotsToCoefs290
Boot circuit total (prepended)12531
Persistent q0 (appended)150
Special primes (HYBRID)num_special_primes61 × num_special_primes
Your full_q_list_precisionyoursyours
Checklist when choosing n: total modulus ≈ 531 + 50 + your full_q_list_precision + 61 × num_special_primes. Enabling bootstrapping lowers security at fixed n; you will generally need a larger n to compensate. Special primes are easy to overlook: nine of them are already 549 bits, more than the boot circuit alone.
You do not need to append a final prime yourself: q0 can serve as the last row after your last rescale when log2(pt_scale) < 50 (in practice target ≤ 45).

Keys

Square key. Needed for polynomial / multiply stages inside the bootstrap.
Rotation key. Needed for slot↔coefficient transforms and sparse-packing subsum rotations.
Dense↔sparse switching keys. Two additional keys generated with the pipeline. The sparse key used inside bootstrapping is managed internally; sk_hw is not a parameter you set.

Precision

Bootstrapping is approximate. On inputs in [-1, 1], expect about 19 bits mean accuracy and 14 bits in the worst slot. This error adds to noise already present and accumulates across repeated bootstraps, so budget for it in long pipelines.

Example

Place a refresh; the compiler enables bootstrapping:
pipeline.pyPYTHON
from lattica_build.params import HomParams, BootstrappingVariant
from lattica_build.base_classes.hom_pipeline import HomomorphicPipeline
from lattica_build.operators import Bootstrap

LOG_N = 16

hom_params = HomParams(
    n=2 ** LOG_N,
    full_q_list_precision=((60,), (60,), (60,), (60,)),
    pt_scale=2 ** 30,
    num_special_primes=6,
    num_init_rows=0,
    bootstrapping_variant=BootstrappingVariant.REAL,
)

pipeline = HomomorphicPipeline(
    hom=Bootstrap(),
    input_shape=(2 ** (LOG_N - 1),),
)

See also

  • Level budget - depth, the chain, and when you need more levels than one chain can hold
  • Choosing parameters - sizing full_q_list_precision, n, and related HomParams
  • Evaluation key - rotation and square key material
  • HomModSwitch - drop a single level without refreshing the chain
  • HomRingSwitch - lift a sub-ring query into the computation ring; its log_n_subring is the client encryption ring, not this sparse-packing parameter