Skip to content

zcash_pool_migration: Let a signer's capacity size one run - #2970

Merged
dannywillems merged 2 commits into
mainfrom
dw/pool-migration-signer-capacity
Aug 17, 2026
Merged

zcash_pool_migration: Let a signer's capacity size one run#2970
dannywillems merged 2 commits into
mainfrom
dw/pool-migration-signer-capacity

Conversation

@dannywillems

Copy link
Copy Markdown
Contributor

Stacked on #2962 (base dw/pool-migration-note-cap); review that one first.

The problem

A per-run note cap cannot bound what an external signer is asked to sign in one
interaction. A run's signing workload is 16 * preparations + 3 * transfers,
and the preparation count follows the wallet's FRAGMENTATION rather than the
number of notes the run mints. So one cap gives one wallet a single-round run
and another a four-round run, and a Keystone user sees several QR signing rounds
inside what the UI presents as one migration run.

signing_round_count(budget) already reports that after the fact. Nothing could
ask for it in advance.

The change

signing_rounds::RunSigningCapacity states the bound a device actually has —
actions per interaction, interactions per run, and a note ceiling never to
exceed — and signing_rounds::largest_run_size_within turns it into a note cap
by searching for the largest run that fits.

That search is the inverse of the round count in the run SIZE, as the existing
min_budget_for_rounds is its inverse in the BUDGET; the two now sit together.
It probes the sizing oracle logarithmically (once when the whole run already
fits) and only ever returns a cap a probe accepted, so an arbitrary preparation
portfolio still gets a run that fits even where the round count is not monotone
in the cap. Under monotonicity — what the canonical decomposition gives, since
raising the cap extends the split rather than reshaping it — the two are adjoint
and the chosen cap is the largest run the signer signs.

The one case no sizing can fix is a wallet fragmented enough that minting even a
single funding note overflows a round. The run is planned anyway and the
overflow stays visible in MigrationPlan::signing_round_count.

API

Both bounds are first-class and nothing existing changes shape:

entry point bound
plan_migration default note cap (unchanged)
plan_migration_with(portfolio, max_notes, ..) note count (unchanged signature)
plan_migration_for_signer(capacity, ..) signer capacity
plan_migration_for_signer_with(portfolio, capacity, ..) signer capacity + portfolio
plan_migration_sized_with(portfolio, sizing, ..) either, as a RunSizing value

estimate_migration_runs* mirrors all five. Sizing is per run, over the note
structure that run starts from, so the preview describes the runs the planner
will build: a wallet migrates fewer notes in the runs that consolidate its
fragmentation and more once its notes are larger.

RunSizing is public so an application that lets the user choose between the
two bounds can carry that choice as a value.

Testing

cargo test -p zcash_pool_migration --all-features (284 unit + every
integration test), cargo fmt --check, cargo clippy --all-features --all-targets -D warnings, cargo check --workspace --all-features and
--no-default-features, all clean.

The search's unit tests hold it to the adjunction against an explicit oracle and
to feasibility against a deliberately non-monotone one. tests/signer_capacity.rs
drives the wallet-facing API from outside the crate: the scenarios that need
several Keystone rounds under the note cap plan in one round when sized for the
signer, the signer-sized plan is exactly the note-capped plan at the chosen cap
(so sizing only picks a cap, it never reshapes a run), one entry point reaches
either bound, and every run the estimate forecasts is one signing round.

🤖 Generated with Claude Code

dannywillems and others added 2 commits August 14, 2026 11:39
A note cap cannot bound what a hardware signer is asked to sign. A run's
actions are `16 * preparations + 3 * transfers`, and the preparation count
follows the wallet's FRAGMENTATION rather than the number of notes the run
mints, so one cap yields a one-round run for one wallet and a four-round run
for another. Sizing by the cap is why a Keystone user sees several QR signing
rounds inside a single run.

`RunSigningCapacity` states the bound the device actually has — actions per
interaction, interactions per run, and the note ceiling never to exceed — and
`largest_run_size_within` turns it into a note cap by searching the largest run
that fits. It is the inverse of the round count in the run SIZE, as
`min_budget_for_rounds` is its inverse in the BUDGET; both now sit in
`signing_rounds`. The search probes the sizing oracle logarithmically and only
ever returns a cap a probe accepted, so an arbitrary preparation portfolio
still gets a run that fits even where the round count is not monotone in the
cap. Under monotonicity — what the canonical decomposition gives, since raising
the cap extends the split rather than reshaping it — the two are adjoint and
the cap is the largest run the signer signs. A wallet fragmented enough that
minting even one funding note overflows a round is the one case no sizing can
fix; the run is planned anyway and the overflow is visible in
`signing_round_count`.

Both bounds are first-class and nothing existing changes shape.
`plan_migration` and `plan_migration_with` keep sizing by note count with the
same signatures; `plan_migration_for_signer{,_with}` size by capacity; and
`plan_migration_sized_with` takes the choice as a `RunSizing` value, for an
application that lets the user pick. The estimate mirrors all of it, sizing
each run over the note structure that run starts from, so the preview describes
the runs the planner will build.

The tests pin both halves. The search's unit tests hold it to the adjunction
against an explicit oracle, and to feasibility against a deliberately
non-monotone one. An integration test drives the wallet-facing API as an SDK
sees it: the scenarios needing several Keystone rounds under the note cap plan
in one round when sized for the signer, the signer-sized plan is exactly the
note-capped plan at the chosen cap, and every run the estimate forecasts is one
signing round.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dannywillems
dannywillems force-pushed the dw/pool-migration-signer-capacity branch from 50941fc to a0cd2a8 Compare August 14, 2026 14:41
noop-sk
noop-sk previously approved these changes Aug 17, 2026
@dannywillems
dannywillems changed the base branch from dw/pool-migration-note-cap to main August 17, 2026 12:18
@dannywillems
dannywillems dismissed noop-sk’s stale review August 17, 2026 12:18

The base branch was changed.

@dannywillems
dannywillems merged commit 0891e00 into main Aug 17, 2026
50 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants