Skip to content

Deterministic transaction keys from an outgoing view key and the spent key images #11140

Description

@mahnunchik

Problem

tx_key and additional_tx_keys are random (device_default::open_txrandom_scalar) and survive only in the wallet cache, gated on store-tx-info. They are cleared by rescan_bc (wallet2::clear_soft), absent after a restore from seed, and never reach a view-only wallet whose transactions are signed elsewhere — sign_tx generates fresh ones.

ZtM2 §2.3 states the consequence: "Anyone who deletes or loses their transaction private key will be unable to make an OutProof." ReserveProofV2 needs them too.

This is #6638.

Proposal

Derive them. Reuse the construction already in tree for multisig (src/multisig/multisig_tx_builder_ringct.cpp#L247-L301), replacing its per-attempt random entropy with a wallet secret:

k_ovk = H_s("TXKEY_OVK"  || k^s)                              outgoing view key
seed  = H ( H("TXKEY_SEED") || k_ovk || I_1 || … || I_N )
h_1   = H_s( seed || H("TXKEY_CHAIN") )
h_j   = H_s( seed || h_{j-1} )

tx_key = h_1,   additional_tx_keys = h_2 … h_{n+1}

I_* are the key images of the spent inputs, in vin order.

Every input to this is recoverable later: the key images from vin, the number of additional keys from the count of additional pubkeys in extra, and k_ovk from the seed. Nothing is stored.

Why key images

  • Unique by consensus, so r cannot repeat and no two outputs can collide — the same argument multisig_tx_builder_ringct.cpp#L859-L865 already makes.
  • Present in the finished transaction, unlike the spent outputs, which are indistinguishable from decoys.
  • Fixed before the outputs exist, so the derivation is not circular, and unchanged when decoys are re-selected.

Improvements over #2366

#2366 proposed r = H_s(k^s || I_1 || … || I_N) and was closed because subaddresses, which postdate it, set R = r·K^{s,i} for a single-subaddress destination, leaving a restored wallet unable to check a derived r against the on-chain R.

  1. Additional keys. [OBSOLETE] deterministic txkey from key images and spend secret key #2366 derives only r. Subaddress destinations need one per output; the chain above yields them from the same seed.
  2. The R = r·K^{s,i} objection does not reach how r is used. Every consumer already requires the recipient's address — check_tx_key takes it as a parameter, and ZtM2 §2.3 lists it among what an OutProof prover must know. So the meaningful check is against the outputs, K^o_t == H_s(r K^v || t)·G + K^s plus the commitment check in decodeRct, not against R. Where R genuinely must be checked (set_tx_key), test r·G == R and fall back to r·K^{s,i} == R using the supplied address; a match on the fallback also confirms it was the single destination.
  3. Not the spend key. The derivation secret is a dedicated outgoing view key, so proof generation can be delegated without granting spend authority.
  4. No new construction — the hash chain is already in tree and reviewed.

Answers to #6638

"knowing tx private keys isn't enough, as the recipient addresses are also required" (@UkoeHB) — correct, and it bounds the feature rather than defeating it. It holds for "list where my payments went", which needs stored addresses regardless. It does not hold for "did tx T pay address X", where X is supplied by whoever is asking — disputes, audits, exchange support, refund claims. That case becomes answerable from the seed alone.

Spend proofs instead (@moneromooo-monero, @stoffu) — SpendProofV1 proves a transaction is yours, not whom it paid.

RNG hardening (@SarangNoether) — applies: a weak RNG at this one point silently discloses recipients.

The cost

Determinism removes the ability to forget. Today a lost cache or a rescan_bc destroys the keys; afterwards anyone obtaining k_ovk, or k^s from which it derives, recovers every outgoing tx key the wallet ever produced, confirming every recipient and decrypting every payment ID. The seed is backed up on paper and kept forever, so the exposure is more durable than the cache it replaces.

This objection was raised on both #2366 and #6638 and does not go away. It argues for a wallet setting, off by default, not against the feature.

Second-order: two transactions over the same input set derive the same keys. They cannot both confirm, so there is no burning-bug risk; the residual is that disclosing r for an abandoned attempt also discloses the confirmed one. Note this inverts the multisig builder's intent, which adds entropy precisely so attempts differ.

Scope

Software wallet only. hw::device::open_tx(crypto::secret_key&) receives neither the inputs nor the account keys, so covering hardware would mean changing that interface plus firmware on Ledger and Trezor; Trezor already has its own recovery path via m_tx_device. Proposal: derive in construct_tx_and_get_tx_key, where the sources are available, and leave device_ledger and device_trezor on current behaviour.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions