Neptune-Crypto / neptune-core

anonymous peer-to-peer cash
Apache License 2.0
25 stars 7 forks source link

use different spending keys for different UTXOs #115

Open aszepieniec opened 7 months ago

aszepieniec commented 7 months ago

Right now, the node only ever uses one spending key. The receiving address is deterministically derived from this spending key, which means in particular that unless you generate a whole new wallet, you have the same receiving address for ever. Expenditures to the same address can be linked.

The advantage of the current approach is convenience from the point of view of implementation. Otherwise we need to keep track of a bunch of different spending keys and a bunch of different ways to unlock UTXOs. Nevertheless, this task must be done to serve user privacy.

There are probably other places that also need to be changed but one place to start is in state/mod.rs in GlobalState::create_transaction.

dan-da commented 6 months ago

Would it make sense to continue with a single spending key and derive receiving keys based on an incrementing counter/nonce that is stored in the wallet? So it is still deterministic on the sender's side to generate a new key for each receive (based on the counter) that is given to the 3rd party. When a new block is encountered, it would need to be checked for transactions that match any of our receive keys up to the counter.

I think I just described the basics of bip32....

aszepieniec commented 6 months ago

This solution makes perfect sense and is more or less what I had in mind also.

A downside of this approach (?): right now you can reduce your entire Neptune treasury to a single seed phrase. As long as you securely store the seed phrase, you will be able to recover your entire Neptune balance (assuming some peer is willing to serve you historical blocks or assuming you trust the peer to scan them for you). But if you can receive Neptune on various addresses generated in this fashion, then I see no way to set the counter to the right number. Either it is set too low (and you risk not observing transactions benefiting you) or it is set too high (and you or your peer do redundant scanning work).

With regards to BIP32, I thought the selling point was that canyone can derive a child receiving addresses from a master receiving address, such that only the holder of the matching master spending key can generate the matching child spending key. Neptune does not have the requisite homomorphisms to accomplish this feat. That said, the added value of this feature is rather limited.

dan-da commented 2 months ago

But if you can receive Neptune on various addresses generated in this fashion, then I see no way to set the counter to the right number.

This is not a problem unique to Neptune. The same exists for bitcoin, monero, etc. I've written key generators for both and there is always the possibility that funds have been sent to some address at a higher index. But that is why a standard gap limit exists, and settings for gap limit, and external tools for scanning/checking. In other words, there doesn't seem to be any perfect solution, but there are "good enough" solutions that are already widely used.

With regards to how neptune would deal with it in the wallet:

Wallet_State::next_unused_spending_key(&mut self, key_type: KeyType) bumps internal counter for key_type and returns key at the new index.

Of course there is no guarantee that:

Wallet_State::get_all_known_keys() returns all keys from [0..counter] for each key-type.

We would also have a gap-limit setting to use when scanning keys, eg when we implement "import seed" functionality and need to scan entire blockchain.

With regards to BIP32, I thought the selling point was that canyone can derive a child receiving addresses from a master receiving address, such that only the holder of the matching master spending key can generate the matching child spending key. Neptune does not have the requisite homomorphisms to accomplish this feat. That said, the added value of this feature is rather limited.

That's an interesting point about xpriv, xpub. Yeah, it's neat that xpub can be given to 3rd parties and they can then derive pub-keys and addresses. There is also a danger with it, as iirc xpub+any derived private-key can reveal xpriv.

bip32 also defines derivation paths which can be useful (but have also been a source of confusion between wallets).

I wonder if we can devise similar mechanisms for our keys.

Anyway though, to me those are not the most compelling feature of bip32. The single most important thing is simply seed + key-derivation. The fact that we can have a single wallet seed that the user can store offline, and then the software (or hardware) derives unique keys from that for use in each tx.

I remember bitcoin-core (just bitcoin then) in the bad old days before bip32. By default it generated 100 keys and it would hand them out with each call to new_address(). These keys were stored in wallet files and people were supposed to back them up. But eventually the 100 keys would be exceeded and new keys were silently generated. Now funds, including change, is being sent to addresses that do not exist in the wallet backup. ouch. A lof of people got bitten that way.

There was also no standard way to move a set of keys between wallets or to backup an entire wallet offline. With a bip32/bip39 seed-phrase it finally became easy to (a) backup a wallet by hand, and (b) import the seed phrase into a wallet on anther device, or even wallet-software by another author. This is where it's important to define clear standards for interop.


anyway, I've already been taking steps towards enabling key derivation. WalletState::next_unused_spending_key(&mut self, ...) now exists and is being called. For now It always returns the key at index 0, because it was requested to keep doing that for testing purposes. We only need to implement that function properly (storing counter to wallet DB) and then unique-key-per-utxo should "just work".