zcash_pool_migration: Let a signer's capacity size one run - #2970
Merged
Conversation
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
force-pushed
the
dw/pool-migration-signer-capacity
branch
from
August 14, 2026 14:41
50941fc to
a0cd2a8
Compare
noop-sk
previously approved these changes
Aug 17, 2026
noop-sk
approved these changes
Aug 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 couldask for it in advance.
The change
signing_rounds::RunSigningCapacitystates 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_withinturns it into a note capby 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_roundsis 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:
plan_migrationplan_migration_with(portfolio, max_notes, ..)plan_migration_for_signer(capacity, ..)plan_migration_for_signer_with(portfolio, capacity, ..)plan_migration_sized_with(portfolio, sizing, ..)RunSizingvalueestimate_migration_runs*mirrors all five. Sizing is per run, over the notestructure 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.
RunSizingis public so an application that lets the user choose between thetwo bounds can carry that choice as a value.
Testing
cargo test -p zcash_pool_migration --all-features(284 unit + everyintegration test),
cargo fmt --check,cargo clippy --all-features --all-targets -D warnings,cargo check --workspace --all-featuresand--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.rsdrives 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