Skip to content

Key image not found after importing key images from air-gapped signer #3111

@phrontizo

Description

@phrontizo

Summary

After importing key images from an air-gapped signer (tested with both Cupcake and a third-party signer), Cake Wallet immediately shows "unexpected error: Key image not found" when attempting to create the transaction. Closing the send flow and retrying the send works on the second attempt without needing another sync.

Steps to Reproduce

  1. Set up a view-only XMR wallet in Cake Wallet paired with an air-gapped signer (Cupcake or other)
  2. Receive XMR to the wallet, wait for confirmations
  3. Initiate a Send in Cake Wallet
  4. Cake shows "Outputs (all)" / "Outputs (partial)" toggle and displays the outputs QR
  5. Scan the outputs QR with the air-gapped signer
  6. The signer displays key images QR
  7. Scan the key images QR with Cake Wallet
  8. Result: "Transaction Error: unexpected error: Key image not found"

Workaround: Close the error, close the entire send flow, tap Send again. The second attempt skips the outputs sync (key images are now present) and proceeds directly to transaction creation, which succeeds.

Root Cause Analysis

The issue is a stale unspentCoins cache in Cake's send flow:

  1. Before the sync flow: unspentCoins is populated via updateUnspent()CoinsImpl::refresh(). For a view-only wallet, each transfer's m_key_image is a locally-computed placeholder (not the real key image from the signer), with m_key_image_known = false. These placeholder key images are stored as hex strings in unspentCoins.

  2. After key image import: importKeyImagesUR calls wallet2::import_key_images which overwrites m_transfers[n].m_key_image with the real key images and sets m_key_image_known = true. The import succeeds.

  3. Transaction creation: createTransaction() checks if (unspentCoins.isEmpty) — but unspentCoins is not empty (populated in step 1), so updateUnspent() is not called. The stale placeholder key image hex strings are passed as preferredInputs to the C FFI.

  4. In C++ createTransactionMultDest (coin-control patch): For each preferred input, frozen(keyImage)get_transfer_details(ki) iterates m_transfers looking for a matching td.m_key_image. The stale placeholder key images no longer match any transfer (they were overwritten in step 2), so CHECK_AND_ASSERT_THROW_MES(false, "Key image not found") is thrown.

  5. On the second attempt: The failure path clears/invalidates unspentCoins, so updateUnspent() runs fresh with the real key images. The transaction succeeds.

Suggested Fix

After importKeyImagesUR returns successfully, clear or refresh unspentCoins so that subsequent createTransaction calls use the real key images:

// In the UR page handler after importing key images:
final result = await monero!.importKeyImagesUR(animatedURmodel.wallet, ur);
if (result) {
  // Refresh the coin cache with real key images
  await monero!.updateUnspent(animatedURmodel.wallet);
  Navigator.of(context).pop(true);
}

Alternatively, createTransaction could always call updateUnspent() rather than conditionally checking unspentCoins.isEmpty.

Environment

  • Cake Wallet v6.0.2 (latest, iOS)
  • Tested with both Cupcake v1.1.0 (macOS) and a third-party air-gapped signer
  • Monero mainnet
  • Same bug occurs with both "Outputs (all)" and "Outputs (partial)" modes
  • Same bug occurs with both "all key images" and "partial key images" from the signer

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions