You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
tx_key and additional_tx_keys are random (device_default::open_tx → random_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.
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:
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.
#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.
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.
Not the spend key. The derivation secret is a dedicated outgoing view key, so proof generation can be delegated without granting spend authority.
No new construction — the hash chain is already in tree and reviewed.
"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.
Problem
tx_keyandadditional_tx_keysare random (device_default::open_tx→random_scalar) and survive only in the wallet cache, gated onstore-tx-info. They are cleared byrescan_bc(wallet2::clear_soft), absent after a restore from seed, and never reach a view-only wallet whose transactions are signed elsewhere —sign_txgenerates 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:I_*are the key images of the spent inputs, invinorder.Every input to this is recoverable later: the key images from
vin, the number of additional keys from the count of additional pubkeys inextra, andk_ovkfrom the seed. Nothing is stored.Why key images
rcannot repeat and no two outputs can collide — the same argumentmultisig_tx_builder_ringct.cpp#L859-L865already makes.Improvements over #2366
#2366 proposed
r = H_s(k^s || I_1 || … || I_N)and was closed because subaddresses, which postdate it, setR = r·K^{s,i}for a single-subaddress destination, leaving a restored wallet unable to check a derivedragainst the on-chainR.r. Subaddress destinations need one per output; the chain above yields them from the same seed.R = r·K^{s,i}objection does not reach howris used. Every consumer already requires the recipient's address —check_tx_keytakes 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^splus the commitment check indecodeRct, not againstR. WhereRgenuinely must be checked (set_tx_key), testr·G == Rand fall back tor·K^{s,i} == Rusing the supplied address; a match on the fallback also confirms it was the single destination.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_bcdestroys the keys; afterwards anyone obtainingk_ovk, ork^sfrom 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
rfor 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 viam_tx_device. Proposal: derive inconstruct_tx_and_get_tx_key, where the sources are available, and leavedevice_ledgeranddevice_trezoron current behaviour.