Skip to content

test framework: Orchard -> Ironwood migration test scaffolding#172

Draft
dannywillems wants to merge 23 commits into
mainfrom
feat/ironwood-migration-test-scaffold
Draft

test framework: Orchard -> Ironwood migration test scaffolding#172
dannywillems wants to merge 23 commits into
mainfrom
feat/ironwood-migration-test-scaffold

Conversation

@dannywillems

Copy link
Copy Markdown
Contributor

Adds qa/rpc-tests/wallet_ironwood_migration.py: scaffolding for the Orchard -> Ironwood value-pool migration flow (NU6.3). It stands up the regtest fixtures (zebrad + zaino + zallet), mints a real Orchard note before activation, crosses the NU6.3 boundary so the Ironwood pool is live, and marks TODO(migration) / TODO(assert) points where the still-evolving zallet migration RPC and the zcash_ironwood_migration_backend planner will plug in. It is deliberately independent of the final API: the {1,2,5}*10^k denomination policy is not pinned, and the test self-skips (exit 0) when no migration RPC is exposed. Registered in DISABLED_SCRIPTS so the suite does not run it until the RPC lands.

Draft; part of the Ironwood migration effort. Do not merge yet.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

dannywillems and others added 14 commits July 16, 2026 13:54
Add wallet_ironwood_migration.py, scaffolding for the Orchard -> Ironwood
value-pool migration (NU6.3, ZIP 2005). The migration itself is still
being built: zallet will expose the command, and the librustzcash
note-split planner crate zcash_ironwood_migration_backend is still
evolving. The module therefore registers the end-to-end flow and its
fixtures without asserting any final behavior, so it stays independent of
the final zallet/librustzcash API.

It reuses the deferred-activation pattern from
wallet_ironwood_activation.py to mint a real v2 Orchard note as the
migration source, then marks TODO points where the migration command and
its post-conditions will plug in. The denomination policy is deliberately
not pinned, since it is still in flux.

Two things keep it from failing the suite while unimplemented: it is
listed in DISABLED_SCRIPTS so the runner does not spend a
coinbase-maturity window on a test that can only skip today (the
framework's equivalent of skip/xfail: registered in NEW_SCRIPTS but
excluded from the run lists), and if run directly it self-skips by
probing for the migration RPC and returning cleanly when none is found.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n test

zallet is gaining a generic pool-migration JSON-RPC parameterized by
from_pool/to_pool (backed by the librustzcash zcash_ironwood_migration_backend
note-split planner). Point the scaffolding's capability probe at the real
method names and drive it generically as orchard -> ironwood.

The probe now resolves each migration method (start, status, advance,
cancel, list) from a small per-method candidate list, so it stays robust
while zallet settles the exact names; the start method is the capability
gate. The flow grows in stages:

  * Stage 0 (method absent): self-skip cleanly (exit 0), as before. The
    currently-shipping zallet build lands here.
  * Stage 1 (method present, engine not landed): assert the RPC surface
    only -- start is callable for orchard -> ironwood and either returns a
    plan-summary object or a clear "not implemented yet" response, and an
    unsupported pool pair is rejected with an error.
  * Stage 2 (engine landed): the fund_orchard_source fixture and the TODO
    block mark where the funded end-to-end drive-and-assert goes. The
    denomination policy is deliberately not pinned; assert only
    conservation-style invariants once it is finalized.

Kept registered in NEW_SCRIPTS and listed in DISABLED_SCRIPTS. Running the
module against the current build self-skips and exits 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Exercise z_previewpoolmigration end to end: fund an Orchard note pre-NU6.3,
cross the activation boundary, then assert input validation (an unsupported pool
pair and an unknown strategy are rejected), a well-shaped plan, and value
conservation for both the default and canonical strategies. The denomination
policy is deliberately not pinned, so the test survives strategy tuning. Stage 0
self-skips when the running zallet lacks the method, so it is safe against older
builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The zallet preview RPC was rewired onto the migration engine's planning slice
(zcash_pool_migration_backend::engine::plan_migration), which changes the
response contract. Update the test to match:

- Drop the strategy parameter and its validation: the engine no longer exposes
  a denomination-strategy choice, so the method takes only account, from_pool,
  to_pool, and an optional minconf.
- Assert the new plan shape: funding_notes carrying each note's crossing value,
  self-funding output, and transfer schedule (broadcast height and expiry);
  plus the note_split and preparation summaries.
- Replace the old single-transaction conservation identity (which no longer
  holds now that preparation is a multi-transaction, fee-charging, reconciled
  plan) with honest invariants: each output funds its crossing plus the fixed
  transfer fee buffer, the migratable total is the sum of the crossings and
  never exceeds the balance, funding notes are a subset of the raw split, and
  every transfer is scheduled at or after the chain tip with an expiry beyond
  its broadcast height.
- The reserved prep fee is now the padded ZIP-317 preparation-transaction fee
  (PREP_TX_ACTIONS * marginal fee), not the ZIP-317 minimum.

The Stage-0 self-skip is unchanged, so the test stays safe against zallet
builds that predate the preview RPC.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the fully-wired z_previewpoolmigration test, and update the migration
scaffolding entry for the renamed engine crate (zcash_ironwood_migration_backend
-> zcash_pool_migration_backend).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add ZIP-318 denomination-structure invariants to the migration preview
test: every crossing in the raw note split is a canonical {1,2,5}*10^k
value in zatoshi (down to the sub-1-ZEC dust floor, so not necessarily a
whole ZEC) and within the denomination cap, the split is non-increasing (a
descending greedy decomposition) summing to its migratable total, and each
scheduled funding note is a canonical denomination. Also assert the shape
is deterministic: a second preview of the same wallet yields the same
multiset of crossings, since only the randomized transfer schedule varies.

Mark the migration preview and generic migration test scripts executable
so the test runner can exec them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the Orchard -> Ironwood preview test with the cases a real caller
hits, all asserted against the RPC's actual behavior and verified
end-to-end on the zebra backend:

- Realistic bad requests are rejected with an RPC error, never a plan:
  migrating a pool to itself, the reverse (Ironwood -> Orchard) and other
  unsupported directions, an unsupported source pool (Sapling), and a
  nonexistent account (in addition to the existing unknown-pool-name case).
- A minconf larger than any recent note's confirmations leaves nothing
  spendable, so a funded account correctly reports nothing to migrate
  rather than a stale plan (exercises the preview's minconf filter and
  its empty-balance path).
- The preview is side-effect-free: after previewing and every rejected
  request, the account's spendable Orchard balance is unchanged and no
  Ironwood notes have been minted.

Also note in the source fixture's docstring that the single funded note
is split by the engine into the several funding notes/crossings the plan
asserts, which is the common real-world case (a wallet consolidates,
then migrates).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…inconf

More real-world preview cases, all asserted against the RPC's actual
behavior and verified end-to-end on the zebra backend:

- Previewing BEFORE NU6.3 activates is rejected with a clear "network
  upgrade required" error (the activation check runs before the balance,
  so a funded account still gets it) -- the case a user hits by trying to
  migrate early.
- The account resolves by its ZIP-32 index as well as its UUID; both name
  the same account and yield the same plan.
- A modest minconf still includes the well-confirmed source note (the
  included side of the confirmation filter), complementing the existing
  unreachable-minconf case on the excluded side.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ming

The start/status/list methods are now registered as scaffold stubs, so
this lifecycle test runs Stage 1 rather than self-skipping. Two changes:

- Cross the NU6.3 boundary before the Stage-1 surface checks. The start
  method validates the pool pair (which requires the upgrade) before
  returning its scaffold response, so running the check pre-activation
  got an "upgrade not active" error instead of the expected scaffold
  answer. Activate first so the surface checks reach the real response.
- Assert the status and list methods' surface too (callable, returning a
  structured answer or a clear "not implemented yet"), the read-only half
  of the lifecycle surface assertable before the engine can run a
  migration. status takes a migration id, so probe it with a placeholder.

Also enumerate the funded Stage-2 lifecycle scenarios in the run_test
TODO -- the full run, resume-after-restart, status/list reflecting
progress, and expiry/reorg recovery -- so the drive-and-assert is
specified for when the engine's commit/broadcast slices land in zallet.

Verified end-to-end on the zebra backend: the surface checks pass against
the current stubs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The zallet pool-migration RPC interface is stable, so the per-method
lists of candidate names (a hedge from when the naming was unsettled)
are collapsed to a single fixed name each: START_RPC, STATUS_RPC,
ADVANCE_RPC, CANCEL_RPC, LIST_RPC, and PREVIEW_RPC. The list-probing
resolve_rpc_name helper becomes resolve_method, which probes one name
and returns it or None. Behavior is unchanged (the real names were
always the first candidate); both tests still pass end-to-end on the
zebra backend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the RPC-surface stub flow with the real funded lifecycle now that
z_startpoolmigration and z_advancepoolmigration are wired: fund an Orchard
source pre-NU6.3, cross the boundary, start the migration, then advance one
step per call (mining and syncing between steps) until it completes, asserting
that Ironwood notes appear and the Orchard balance drains.

The drive gates on the preview's layer_count: commit_preparation currently
supports only single-layer preparation, and plan_migration produces a
multi-layer (3-layer) preparation for every balance tried (1 to 137 ZEC), so
the test skips cleanly with a clear message rather than failing. Once
multi-layer preparation is supported the drive runs unchanged.

Finding to investigate: the engine's standalone end-to-end test gets a single
layer for 78 ZEC, but zallet's plan_migration over the funded wallet gets three
layers for the same nominal balance; the wallet's actual Orchard note set (the
source note plus any change/dust) likely differs from the one clean note the
engine test uses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The engine now commits a multi-layer preparation phase by phase (each later
layer built once its predecessor mines), so the test no longer skips balances
that fan out across layers. Remove the single-layer preview gate and let the
advance loop drive the whole lifecycle. Verified on a zebra regtest node: a
137 ZEC Orchard source produces a 3-layer preparation and 10 funding notes,
and the migration completes with 10 Ironwood notes minted and the Orchard
balance drained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LIST_RPC = 'z_listpoolmigrations'


def orchard_notes(wallet, minconf=1):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be moved into utils.py


# ---- capability probing ------------------------------------------------

@staticmethod

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be moved in the TestFramework class directly.

return (e.error.get('code') == RPC_METHOD_NOT_FOUND
or 'method not found' in msg)

def resolve_method(self, w, name, *probe_args):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be moved in the TestFramework class directly.


# Report which of the companion methods this build exposes
# (informational; only `start` gates the flow).
status_name = self.resolve_method(w, STATUS_RPC, UNSUPPORTED_POOL)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we not use directly the method name?

# boundary so the migration is enabled.
acct = w.z_listaccounts()[0]['account_uuid']
print("Funding an Orchard source note pre-NU6.3...")
orchard_before = self.fund_orchard_source(node, w, taddr, acct)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be nice to generalize this method, with a number of notes and the total balance.

dannywillems and others added 9 commits July 18, 2026 19:59
A catalog of real-world integration-test scenarios for the pool migration:
personas (small holder, retail, whale, exchange), note distributions
(single, many, dust-heavy, mixed), the multi-layer anchor-bucket dependency
invariant, the mobile-wallet next-action/resume surface, and edge cases
(reorg, expiry, cancel, stall, concurrent deposit, ceiling, minconf,
pre-activation, read-only). One scenario per file once agreed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record that the migration state machine, state transitions, advance decision,
and next-actions/status view live in the librustzcash engine crate (the mobile
wallet consumes it directly; zallet is a thin consumer), so scenarios assert
shared-engine behavior through the zallet regtest harness. Note the two existing
tests are the first harness entries and are activated once the catalog is green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The transfers are broadcast on a randomized privacy schedule, so a run can need
more advance steps than a tight schedule; raise the step and per-step mine
budget so the lifecycle completes for any schedule spread (verified end to end:
a 137 ZEC source, 3-layer preparation, completes at step 100 with 10 Ironwood
notes). Add ironwood_migration_common.py, the shared scenario harness: funding
by wallet shape, the advance/mine drive loop, and asserters for value
conservation, the multi-layer anchor-bucket ordering, and the engine's
machine-readable next-actions surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…enarios

Two fast validation scenarios on the shared harness: starting a migration for
an account with no spendable source-pool balance is rejected cleanly (D8), and
a second start while one is in progress is refused so an in-flight migration's
pre-signed transactions are never discarded (D12). Both verified on a regtest
zebra node.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Status, advance, and cancel must reject an id that does not name the wallet's
migration, both with no migration in progress and while one is running, so a
wallet polling or resuming with a stale id gets a clear error rather than a
crash or a silent no-op. Verified on a regtest zebra node.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drive a multi-layer migration and, at every advance step, assert the engine's
machine-readable next-actions surface: a ready transaction names an action and
is unblocked; a waiting one reports its blocker; and the anchor-bucket ordering
holds both ways (a later layer is never ready, and is never built, while any
transaction of its predecessor layer is unmined). Fold the anchor-bucket check
into the per-snapshot consistency assertion (a built layer-N implies a mined
layer-(N-1)), which is correct without cross-step timing. Verified end to end:
a 3-layer migration completes with the ordering held across all layers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Starting a migration before NU6.3 is active is rejected with a clear error that
names the required upgrade, even when the account holds a real Orchard note.
Verified on a regtest zebra node.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cancel a migration mid-flight (after some layer-0 transactions are broadcast),
then assert it becomes terminal (phase 'cancelled') and a subsequent advance is
a no-op that does not resurrect it. This surfaced a real engine bug, now fixed:
recompute_status revived a cancelled (Failed) migration to InProgress on the next
advance. Verified on a regtest zebra node with the fixed engine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the balance correctness into each scenario file: the harness provides only
data helpers (ironwood_balance_zat, orchard_balance_zat), and each scenario
asserts inline the source balance, every individual Ironwood note value (via the
valueZat field), that the note count equals the plan's crossing count, value
conservation, the fee bound, and the sub-dust residual, so a reviewer is
convinced by reading one file. Verified: a 137.5 ZEC source crosses into the
canonical 1-2-5 note set summing to the source minus a bounded fee and residual.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant