Skip to content

feat(kaspa): add migration command for escrow key rotation#433

Closed
danwt wants to merge 543 commits intomainfrom
danwt/claude/kaspa-rotation-migration-command
Closed

feat(kaspa): add migration command for escrow key rotation#433
danwt wants to merge 543 commits intomainfrom
danwt/claude/kaspa-rotation-migration-command

Conversation

@danwt
Copy link
Copy Markdown

@danwt danwt commented Jan 12, 2026

Summary

Test plan

  • Unit tests pass
  • Integration test with testnet migration
  • Verify validators sign correctly
  • Verify relayer fee inputs are signed

🤖 Generated with Claude Code

troykessler and others added 30 commits September 22, 2025 12:37
Co-authored-by: Danil Nemirovsky <4614623+ameten@users.noreply.github.com>
Co-authored-by: Danil Nemirovsky <4614623+ameten@users.noreply.github.com>
…7073)

Co-authored-by: Danil Nemirovsky <4614623+ameten@users.noreply.github.com>
…e-xyz#7079)

Co-authored-by: Danil Nemirovsky <4614623+ameten@users.noreply.github.com>
mtsitrin and others added 28 commits December 21, 2025 14:26
chore: fixed cargo warning
refactor(solana): atomic warp route deployment
* claude: refactor(kaspa): cleanup confirmation validator

- Remove stale FIXME comment about address validation. Address validation
  is already performed during withdrawal signing (withdraw.rs:376-394).
  The confirmation validator trusts that withdrawals were properly validated
  at signing time.

- Extract proto_to_outpoint helper function to DRY out duplicated
  TransactionOutpoint conversion code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* claude: simplify proto_to_outpoint by removing field_name arg

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Extract duplicate output pair matching logic into `extract_output_pair` helper
function. Both `prepare_next_iteration_inputs` and `create_inputs_from_sweeping_bundle`
had identical pattern matching to identify escrow vs relayer outputs from a PSKT
with exactly two outputs. The helper now returns an `OutputPair` struct containing
both outputs with their indices.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The Foo(String) variant in HyperlaneKaspaError is never used anywhere
in the codebase - only defined. Remove it to reduce dead code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Extract shared HyperlaneMessage field validation into `validate_hl_message_fields`
function in error.rs. Both deposit.rs and withdraw.rs MustMatch structs now use
this shared function instead of duplicating the same validation logic.

Also removes unused `enable_validation` field and `set_validation` method from
deposit MustMatch (dead code that was never called).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add hex_to_kaspa_hash utility to dym-kas-core (dymension/libs/kaspa/lib/core)
as a shared Kaspa hash conversion utility. Updates three call sites:
- kas_validator/confirmation.rs
- kas_relayer/confirm/confirmation.rs
- kas_relayer/confirm/confirmation_test.rs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
…#422)

Remove redundant 'kas_' prefix from module names since they're already
inside the dymension-kaspa crate. This improves code clarity without
changing functionality.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
…rchain_gas_paymasters (#423)

Update code to use the plural Vec<H256> field name that was changed in
CoreContractAddresses. The scraper now uses .first().cloned().unwrap_or_default()
to get the first IGP address from the array.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
…#424)

* claude: feat(kaspa): unify validator config into single kaspaValidators array

Replace three separate comma-separated config fields with a single array
of validator objects:

Before:
- validatorHosts: "host1,host2,..."
- validatorIsmAddresses: "addr1,addr2,..."
- validatorPubsKaspa: "pub1,pub2,..."

After:
- kaspaValidators: [
    {host: "host1", ismAddress: "addr1", escrowPub: "pub1"},
    ...
  ]

This eliminates the implicit ordering requirement between separate fields
and keeps related validator data together. The RelayerStuff struct now
stores the unified Vec<KaspaValidatorInfo> directly instead of parallel
vectors.

Closes dymensionxyz/hyperlane-deployments#64

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* claude: fix(kaspa): sort signatures by ISM address for Hub ISM verification

The Hub ISM requires signatures to be in lexicographic order of validator
ISM addresses. This commit:

- Updates collect_with_threshold to return (index, value) pairs
- Sorts deposit signatures by recovered signer address
- Sorts confirmation signatures by config ISM address (using index lookup)
- Adds documentation about config ordering requirements
- Adds warning logging for invalid ISM address parsing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
)

* claude: feat(kaspa): add migration mode config and guards for key rotation

Add ValidationConf fields:
- migration_target_address: when set, enables migration mode
- previous_escrow_address: for confirmation across escrow boundary

Add is_migration_mode() helper and guard checks in handlers:
- respond_validate_new_deposits: returns error in migration mode
- respond_sign_pskts: returns error in migration mode
- confirmation endpoint remains enabled for migration

Config parsing updated to support:
- migrationTargetAddress JSON field
- previousEscrowAddress JSON field

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* claude: refactor(kaspa): remove redundant previous_escrow_address

During migration, the current escrow config IS the old escrow.
For confirmation boundary crossing, use migration_target_address
as the new escrow instead of a separate previous_escrow_address.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* claude: refactor(kaspa): rename validation toggles and remove unused config

- Rename deposit_enabled -> validate_deposits
- Rename withdrawal_enabled -> validate_withdrawals
- Rename withdrawal_confirmation_enabled -> validate_confirmations
- Remove unused previousEscrowAddress from config parsing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* claude: feat(kaspa): add migration TX validation and signing endpoint

Add /sign_migration endpoint for validators to sign migration PSKTs
during key rotation. The endpoint:

- Only accepts requests when migration mode is active
- Validates that PSKT spends from current escrow inputs
- Validates all outputs go to configured migration_target_address
- Signs escrow inputs with validator's Kaspa key

Also bumps hyperlane-cosmos-rs to include MigrationFxg proto.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* claude: rewrite migration validation with proper security checks

Migration validation now implements all requirements from issue #192:

- Query hub for current anchor via outpoint() and verify PSKT spends it
- Query Kaspa for ALL escrow UTXOs and verify PSKT spends ALL of them
- Verify PSKT contains ONLY expected inputs (escrow UTXOs + hub anchor)
- Verify exactly ONE output to migration target address
- Verify payload is empty MessageIDs (no withdrawals during migration)
- Validate output amount doesn't exceed input amount

Also:
- Add parsed_migration_target() to ValidationConf for encapsulated parsing
- DRY escrow_input_selector() and calculate_escrow_input_sum() in withdraw.rs
- Pass hub_rpc and kaspa_grpc to migration validation in server.rs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* claude: fix migration validation critical issues

Addresses review feedback:

1. Allow relayer fee inputs - removed "unexpected input" rejection since
   relayer needs to provide inputs to pay transaction fees

2. Verify escrow funds 100% preserved - now checks escrow_input_sum == output
   instead of output <= total_inputs (uses EscrowAmountMismatch error)

3. Verify UTXO amounts against Kaspa - query now returns amounts, and we
   verify each PSKT input amount matches what Kaspa reports for that outpoint

4. Fail on missing UTXO entries - now returns error instead of silently
   defaulting to 0 with map_or()

5. Use calculate_escrow_input_sum() from withdraw.rs for DRY escrow sum

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* claude: extract query_hub_anchor to ops/withdraw.rs

DRY improvement - extracted hub anchor query logic:
- Added parse_hub_outpoint() helper for outpoint conversion
- Added query_hub_anchor() that uses withdrawal_status(vec![], None)
- Updated filter_pending_withdrawals() to use parse_hub_outpoint()
- Migration validation now uses shared query_hub_anchor()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* claude: refactor migration validation and DRY key loading

- Rename sign_withdrawal_fxg to sign_pskt_bundle (serves both uses)
- Extract load_keypair() method on KaspaEscrowKeySource to DRY key loading
- Allow relayer change outputs in migration validation (same pattern as withdrawals)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* claude: revert unintended sealevel formatting changes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add relayer-side migration flow that:
- Fetches all UTXOs from current escrow
- Builds migration PSKT transferring funds to new escrow
- Collects signatures from validators via get_migration_sigs
- Combines bundles using multisig threshold
- Finalizes and broadcasts migration transactions

Also adds:
- ValidatorsClient.get_migration_sigs() for collecting validator signatures
- request_sign_migration_bundle() HTTP client function
- finalize_migration_txs() for migration transaction finalization
- Makes combine_all_bundles() public for reuse

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use imported PopulatedInput type from dym_kas_core::pskt
- Use PopulatedInputBuilder instead of manual construction (fixes sequence value)
- Consolidate create_pskt with optional payload (removes duplicate function)
- Remove redundant threshold check (enforced by collect_with_threshold)
- Remove unnecessary Arc clone

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add migrateEscrowTo setting that enables migration mode. When set,
the relayer executes escrow key migration to the specified Kaspa
address and exits instead of running the normal relayer loop.

Usage: HYP_RELAYER_MIGRATEESCROWTO="kaspa:qz..." ./relayer

Changes:
- Add migrate_escrow_to field to RelayerSettings
- Add migration check at start of Relayer::run()
- Add run_escrow_migration() method
- Refactor execute_migration to take &KaspaProvider
- Re-export KaspaAddress from dymension_kaspa

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move the migration target address from RelayerSettings to the Kaspa-specific
RelayerStuff config object. This is the proper location since migration is
Kaspa-specific functionality.

Changes:
- Add migrate_escrow_to to RelayerStuff in conf.rs
- Parse migrateEscrowTo in connection_parser.rs
- Update relayer to read from KaspaProvider.must_relayer_stuff()
- Remove hacky migrate_escrow_to from RelayerSettings

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extract get_migration_target() helper and pass target address as parameter
to run_escrow_migration() to eliminate duplicate lookups and reduce nesting.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fix PR #432 migration construction to match PR #431 validation:

1. Payload: Use MessageIDs::new(vec![]).to_bytes() instead of None
   - Validation expects empty MessageIDs protobuf, not missing payload

2. Fee handling: Add relayer fee inputs and change output
   - Kaspa requires non-zero fees (fee = inputs - outputs)
   - Escrow funds are 100% preserved (escrow_sum == target_output)
   - Relayer pays fees from their own UTXOs with change returned

3. Hub anchor verification: Check hub anchor is among escrow UTXOs
   - Query hub for current anchor before building PSKT
   - Fail early if state is stale (anchor not in escrow UTXOs)

The migration PSKT now:
- Inputs: all escrow UTXOs + relayer fee UTXOs
- Outputs: migration target (escrow_sum) + relayer change (relayer_sum - fee)
- Payload: empty MessageIDs serialized as protobuf

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Validators only sign escrow inputs, but the relayer fee inputs were
left unsigned, which would cause a panic at runtime during finalization.

Add sign_relayer_fee helper to sign relayer inputs after collecting
validator signatures, matching the withdrawal flow pattern.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@danwt
Copy link
Copy Markdown
Author

danwt commented Jan 13, 2026

Closing: duplicate of #432, accidentally opened against main instead of main-dym

@danwt danwt closed this Jan 13, 2026
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.