diff --git a/.cargo/config.toml b/.cargo/config.toml index 6702cbd8..90b85fa2 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -2,3 +2,8 @@ # see: https://docs.rs/console-subscriber/latest/console_subscriber/ [build] rustflags = ["--cfg", "tokio_unstable"] + +# workaround for rustc 1.80 running out of stack space when building triton-vm +[env] +RUST_MIN_STACK="33554432" + diff --git a/Cargo.lock b/Cargo.lock index 81a28ee3..a5206dc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2778,9 +2778,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -2799,9 +2799,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", diff --git a/Cargo.toml b/Cargo.toml index 72351f47..70c0b96f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ default-run = "neptune-core" publish = false [dependencies] -aead = "0.5" +aead = { version = "0.5", features = ["std"] } aes-gcm = "0.10" anyhow = "1.0" arbitrary = { version = "1.3", features = ["derive"] } diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index f94174d9..092ee7f0 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -9,6 +9,8 @@ - [Syncing](./neptune-core/syncing.md) - [Sequences](./neptune-core/sequences.md) - [Reorganization](./neptune-core/reorganization.md) + - [Keys and Addresses](./neptune-core/keys.md) + - [Utxo Notification](./neptune-core/utxo_notification.md) - [Contributing](./contributing.md) - [Git Workflow](./contributing/git-workflow.md) - - [Git Message](./contributing/git-message.md) \ No newline at end of file + - [Git Message](./contributing/git-message.md) diff --git a/docs/src/neptune-core/keys.md b/docs/src/neptune-core/keys.md new file mode 100644 index 00000000..5578e7fb --- /dev/null +++ b/docs/src/neptune-core/keys.md @@ -0,0 +1,62 @@ +# Keys and Addresses + +`neptune-core` uses an extensible system of keys and addresses. This is accomplished via an abstract type for each. At present two types of keys are supported: `Generation` and `Symmetric`. + +## Abstraction layer + +Three `enum` are provided for working with keys and addresses: + +| enum | description | +|------------------------| -------------------------------------------------| +| `KeyType` | enumerates available key/address implementations | +| `SpendingKeyType` | enumerates key types and provides methods | +| `ReceivingAddressType` | enumerates address types and provides methods | + +note: It was decided to use `enum` rather than traits because the enums can be +used within our RPC layer while traits cannnot. + +Most public APIs use these types. That provides flexibility and should also make it easy to add new implementations in the future if necessary. + +## Root Wallet Seed + +At present all supported key types are based on the same secret `seed`. The end-user can store/backup this seed using a bip39 style mnemonic. + +## Key derivation + +For each key-type, the neptune-core wallet keeps a counter which tracks the latest derived key. + +To obtain the next unused address for a given key type call the rpc method `next_receiving_address(key_type)`. + +(note: as of this writing it always returns the same address at index 0, but in the future it will work as described) + +An equivalent API for obtaining the next unused spending key is available in the neptune-core crate, but is not (yet?) exposed as an rpc API. + + +## Available key types + +`Generation` and `Symmetric` type keys are intended for different usages. + +### `Generation` keys and addresses + +`Generation` keys are asymmetric keys, meaning that they use public-key cryptography to separate a secret key from a public key. + +They are primarily intended for sending funds to third party wallets. They can also be used for sending funds back to the originating wallet but they waste unnecessary space when the encrypted ciphertext is stored on the blockchain. + +|alan todo: describe generation keys/addresses| + +### `Symmetric` keys and addresses + +`Symmetric` keys are implemented with aes-256-gcm, a type of symmetric key, +meaning that a single key is used both for encrypting and decrypting. + +Anyone holding the key can spend associated funds. A symmetric key is equivalent to a private-key, and it has no equivalent to a public-key. + +They are primarily intended for sending funds (such as change outputs) back to +the originating wallet. However additional use-cases exist such as sending between separate wallets owned by the same person or organization. + +Data encrypted with `Symmetric` keys is smaller than data encrypted with asymmetric keys such as `Generation`. As such, it requires less blockchain space and should result in lower fees. + +For this reason change output notifications are encrypted with a `Symmetric` key by default and it is desirable to do the same for all outputs destined for the +originating wallet. + +Note that the `Symmetric` variant of abstract types `SpendingKeyType` and `ReceivingAddressType` both use the same underlying `SymmetricKey`. So they differ only in the methods available. For this reason, it is important never to give an "address" of the `Symmetric` type to an untrusted third party, because it is also the spending key. \ No newline at end of file diff --git a/docs/src/neptune-core/utxo_notification.md b/docs/src/neptune-core/utxo_notification.md new file mode 100644 index 00000000..716b824c --- /dev/null +++ b/docs/src/neptune-core/utxo_notification.md @@ -0,0 +1,125 @@ +# Utxo Notification + +When a sender creates a payment it is necessary to transfer some secrets to the recipient in order for the recipient to identify and claim the payment. + +The secrets consist of a `Utxo` and a `Digest` that represents a random value created by the sender called `sender_randomness`. + +It does not matter *how* these secrets are transferred between sender and receiver so long as it is done in a secure, private fashion. + +There are two broad possibilities: +1. write the secrets to the blockchain, encrypted to the recipient +2. do not write secrets to the blockchain. Use some out-of-band method instead. + +`neptune-core` supports both of these. They are referred to as notification methods. An enum `UtxoNotifyMethod` exists and provides variant `OnChain` and `OffChain`. + +It is also important to recognize that sometimes the sender and receiver may be the same wallet or two wallets owned by the same person or organization. + +## OnChain Utxo transfers + +`OnChain` transfers are performed with the struct `PublicAnnouncement`. It is an opaque list of fields of type `BFieldElement` that can hold arbitrary data. A list of `PublicAnnouncement` are attached to each neptune `Transaction` and stored on the blockchain. + +The neptune key types leverage `PublicAnnouncement` to store the `key_type` in the first field and a unique `receiver_id` in the second field that is derived from the receiving address. These fields are plaintext, so anyone can read them. + +The remaining fields (variable length) are filled with encrypted ciphertext that holds `Utxo` and `sender_randomness` which are necessary to claim/spend the `Utxo`. + +### Identifying `Utxo` destined for our wallet + +#### Illustrating the challenge. + +Given that the notification secrets are encrypted there exists a problem. How can a wallet identify which `PublicAnnouncement` are intended for it? + +The simplest and most obvious solution is to attempt to decrypt the ciphertext of each. If the encryption succeeds then we can proceed with claiming the `Utxo`. While this works it is very inefficient. Each block may contain thousands of `PublicAnnouncement`. Further our wallet may have hundreds or even thousands of keys that must be checked against each announcement, making this an `n*m` operation. While it may be feasible for a node to do this if it is online all the time it becomes very expensive to scan the entire blockchain as may be necessary when restoring an old wallet from a seed. + +We can do better. + +#### How `neptune-core` solves it. + +This is where the `key-type` and `receiver_identifier` of the `PublicAnnouncement` come into play. + +Since these fields are plaintext we can use them to identify notifications intended for our wallet prior to attempting decryption. + +Each `SpendingKeyType` has a `receiver_identifier` field that is derived from the secret-key. This uniquely identifies the key without giving away the secret. As such, it can be shared in the public-announcement. + +The algorithm looks like: + +``` +for each key-type we support: + for each known key in our wallet: + for each public-announcement in the block-transaction: + filter by key-type + filter by key.receiver_id + filter by key.decrypt(announcement.ciphertext) result +``` + +#### Privacy warning + +It is important to note that this scheme makes it possible to link together multiple payments that are made to the same key. This mainly affects `Generation` keys as the address (public-key) is intended to be shared with 3rd parties and it is not possible to prevent 3rd parties from making multiple payments to the same address. + +Wallet owners can mitigate this risk somewhat by generating a unique receiving address for each payment and avoid posting it in a public place. Of course this is not feasible for some use-cases, eg posting an address in a forum for purpose of accepting donations. + +It is planned to address this privacy concern but it may not happen until after Neptune mainnet launches. + + +## OffChain Utxo transfers + +Many types of OffChain transfers are possible. `neptune-core` aims to support the following types at launch: + +1. Local state (never leaves source machine/wallet) + +2. Neptune p2p network + +3. External / Serialized (proposed) + +In the future `neptune-core` or a 3rd party wallet might support using a +decentralized storage mechanism such as IPFS. Decentralized storage may provide a solution for ongoing wallet backups or primary wallet storage to minimize risk of funds loss, as discussed below. + +### Warning! Risk of funds loss + +It is important to recognize that all `OffChain` methods carry an extra risk of losing funds as compared to `OnChain` notification. Since the secrets do not exist anywhere on the blockchain they can never be restored by the wallet if lost during or any time after the transfer. + +For example Bob performs an OffChain utxo transfer to Sally. Everything goes fine and Sally receives the notification and her wallet successfully identifies and validates the funds. Six months later Sally's hard-drive crashes and she doesn't have any backup except for her seed-phrase. She imports the seed-phrase into a new neptune-core wallet. The wallet then scans the blockchain for `Utxo` that belong to Sally. Unfortunately the wallet will not be able to recognize or claim any `Utxo` that she received via `OffChain` notification. + +For this reason, it becomes crucial to maintain ongoing backups/redundancy of wallet data when receiving payments via OffChain notification. And/or to ensure that the OffChain mechanism can reasonably provide data storage indefinitely into the future. + +Wallet authors should have strategies in mind to help prevent funds loss for recipients if providing off-chain send functionality. Using decentralized storage for encrypted wallet files might be one such strategy. + +With the scary stuff out of the way, let's look at some `OffChain` notification methods. + +### Local state. + +Local state transfers are useful when a wallet makes a payment to itself. +Self-payments occur for almost every transaction when a change output is +created. Let's say that Bob has a single `Utxo` in his wallet worth 5 tokens. +Bob pays Sally 3 tokens so the 5-token `Utxo` gets split into two `Utxo` worth 3 +and 2 respectively. The 2-token `Utxo` is called the change output, and it must +be returned into Bob's wallet. + +note: A wallet can send funds to itself for other reasons, but change outputs are predicted to be the most common use-case. + +When a wallet is sending a `Utxo` to itself there is no need to announce this on +the public blockchain. Instead the wallet simply stores a record, called an +`ExpectedUtxo` in local state (memory and disk) and once a block is mined that +contains the transaction, the wallet can recognize the `Utxo`, verify it can be +claimed, and add it to the list of wallet-owned `Utxo` called `monitored_utxos`. + +### Neptune p2p network + +`Utxo` secrets that are destined for 3rd party wallets can be distributed via the neptune P2P network. This would use the same p2p protocol that distributes transactions and blocks however the secrets would be stored in a separate `UtxoNotificationPool` inside each neptune-core node. + +|alan or sword-smith, please flesh this out.| + +### External / Serialized + +note: this is a proposed mechanism. It does not exist at time of writing. + +The idea here is that the transfer takes place completely outside of `neptune-core`. + +1. When a transaction is sent `neptune-core` would provide a serialized `PublicAnnouncement` for each `OffChain` output. + +2. Some external process then transfers the `PublicAnnouncement` to the intended recipient. + +3. The recipient then invokes the `claim_utxos()` RPC api and passes in a list of serialized `PublicAnnouncement`. `neptune-core` then attempts to recognize and claim each `PublicAnnouncement`, just as if it had been found on the blockchain. + +4. Optionally the recipient could pass a flag to `claim_utxos()` that would cause it to initiate a new OnChain payment into the recipient's wallet. This could serve a couple purposes: + * using OnChain notification minimizes future data-loss risk for recipient. + * if the funds were sent with a symmetric-key this prevents the sender from spending (stealing) the funds later. diff --git a/src/bin/dashboard_src/receive_screen.rs b/src/bin/dashboard_src/receive_screen.rs index 162ed212..62dc9184 100644 --- a/src/bin/dashboard_src/receive_screen.rs +++ b/src/bin/dashboard_src/receive_screen.rs @@ -10,7 +10,9 @@ use super::{ screen::Screen, }; use crossterm::event::{Event, KeyCode, KeyEventKind}; -use neptune_core::{config_models::network::Network, rpc_server::RPCClient}; +use neptune_core::{ + config_models::network::Network, models::state::wallet::address::KeyType, rpc_server::RPCClient, +}; use ratatui::{ layout::{Alignment, Margin}, style::{Color, Style}, @@ -65,7 +67,7 @@ impl ReceiveScreen { tokio::spawn(async move { // TODO: change to receive most recent wallet let receiving_address = rpc_client - .next_receiving_address(context::current()) + .next_receiving_address(context::current(), KeyType::Generation) .await .unwrap(); *data.lock().unwrap() = Some(receiving_address.to_bech32m(network).unwrap()); @@ -85,7 +87,7 @@ impl ReceiveScreen { tokio::spawn(async move { *generating.lock().unwrap() = true; let receiving_address = rpc_client - .next_receiving_address(context::current()) + .next_receiving_address(context::current(), KeyType::Generation) .await .unwrap(); *data.lock().unwrap() = Some(receiving_address.to_bech32m(network).unwrap()); diff --git a/src/bin/dashboard_src/send_screen.rs b/src/bin/dashboard_src/send_screen.rs index e6320065..d190e904 100644 --- a/src/bin/dashboard_src/send_screen.rs +++ b/src/bin/dashboard_src/send_screen.rs @@ -14,8 +14,8 @@ use crossterm::event::{Event, KeyCode, KeyEventKind}; use neptune_core::{ config_models::network::Network, models::{ - blockchain::type_scripts::neptune_coins::NeptuneCoins, - state::wallet::address::ReceivingAddressType, + blockchain::{transaction::UtxoNotifyMethod, type_scripts::neptune_coins::NeptuneCoins}, + state::wallet::address::ReceivingAddress, }, rpc_server::RPCClient, }; @@ -83,7 +83,7 @@ impl SendScreen { ) { *focus_arc.lock().await = SendScreenWidget::Notice; *notice_arc.lock().await = "Validating input ...".to_string(); - let maybe_valid_address: Option = rpc_client + let maybe_valid_address: Option = rpc_client .validate_address(context::current(), address, network) .await .unwrap(); @@ -133,7 +133,13 @@ impl SendScreen { const SEND_DEADLINE_IN_SECONDS: u64 = 40; send_ctx.deadline = SystemTime::now() + Duration::from_secs(SEND_DEADLINE_IN_SECONDS); let send_result = rpc_client - .send(send_ctx, valid_amount, valid_address, fee) + .send( + send_ctx, + valid_amount, + valid_address, + UtxoNotifyMethod::OnChain, + fee, + ) .await .unwrap(); diff --git a/src/bin/neptune-cli.rs b/src/bin/neptune-cli.rs index 4bf571ef..22fbf736 100644 --- a/src/bin/neptune-cli.rs +++ b/src/bin/neptune-cli.rs @@ -4,8 +4,10 @@ use clap_complete::{generate, Shell}; use neptune_core::config_models::data_directory::DataDirectory; use neptune_core::config_models::network::Network; use neptune_core::models::blockchain::block::block_selector::BlockSelector; +use neptune_core::models::blockchain::transaction::UtxoNotifyMethod; use neptune_core::models::blockchain::type_scripts::neptune_coins::NeptuneCoins; -use neptune_core::models::state::wallet::address::ReceivingAddressType; +use neptune_core::models::state::wallet::address::KeyType; +use neptune_core::models::state::wallet::address::ReceivingAddress; use neptune_core::models::state::wallet::coin_with_possible_timelock::CoinWithPossibleTimeLock; use neptune_core::models::state::wallet::wallet_status::WalletStatus; use neptune_core::models::state::wallet::WalletSecret; @@ -63,9 +65,9 @@ impl TransactionOutput { pub fn to_receiving_address_amount_tuple( &self, network: Network, - ) -> Result<(ReceivingAddressType, NeptuneCoins)> { + ) -> Result<(ReceivingAddress, NeptuneCoins)> { Ok(( - ReceivingAddressType::from_bech32m(&self.address, network)?, + ReceivingAddress::from_bech32m(&self.address, network)?, self.amount, )) } @@ -194,19 +196,13 @@ async fn main() -> Result<()> { let wallet_dir = data_dir.wallet_directory_path(); DataDirectory::create_dir_if_not_exists(&wallet_dir).await?; - let (wallet_secret, secret_file_paths) = + let (_, secret_file_paths) = WalletSecret::read_from_file_or_create(&wallet_dir).unwrap(); println!( "Wallet stored in: {}\nMake sure you also see this path if you run the neptune-core client", secret_file_paths.wallet_secret_path.display() ); - let spending_key = wallet_secret.nth_generation_spending_key(0); - let receiver_address = spending_key.to_address(); - println!( - "Wallet receiver address: {}", - receiver_address.to_bech32m(network).unwrap() - ); println!( "To display the seed phrase, run `{} export-seed-phrase`.", @@ -418,7 +414,9 @@ async fn main() -> Result<()> { println!("{}", serde_json::to_string_pretty(&wallet_status)?); } Command::OwnReceivingAddress => { - let rec_addr = client.next_receiving_address(ctx).await?; + let rec_addr = client + .next_receiving_address(ctx, KeyType::Generation) + .await?; println!("{}", rec_addr.to_bech32m(args.network).unwrap()) } Command::MempoolTxCount => { @@ -450,9 +448,17 @@ async fn main() -> Result<()> { fee, } => { // Parse on client - let receiving_address = ReceivingAddressType::from_bech32m(&address, args.network)?; - - client.send(ctx, amount, receiving_address, fee).await?; + let receiving_address = ReceivingAddress::from_bech32m(&address, args.network)?; + + client + .send( + ctx, + amount, + receiving_address, + UtxoNotifyMethod::OnChain, + fee, + ) + .await?; println!("Send completed."); } Command::SendToMany { outputs, fee } => { @@ -461,7 +467,9 @@ async fn main() -> Result<()> { .map(|o| o.to_receiving_address_amount_tuple(args.network)) .collect::>>()?; - client.send_to_many(ctx, parsed_outputs, fee).await?; + client + .send_to_many(ctx, parsed_outputs, UtxoNotifyMethod::OnChain, fee) + .await?; println!("Send completed."); } Command::PauseMiner => { diff --git a/src/bin/neptune-dashboard.rs b/src/bin/neptune-dashboard.rs index 83e7fada..df8a9844 100644 --- a/src/bin/neptune-dashboard.rs +++ b/src/bin/neptune-dashboard.rs @@ -5,10 +5,10 @@ /// /// This is a very simple example: /// * A input box always focused. Every character you type is registered -/// here +/// here /// * Pressing Backspace erases a character /// * Pressing Enter pushes the current input in the history of previous -/// messages +/// messages /// use anyhow::{bail, Result}; use clap::Parser; diff --git a/src/mine_loop.rs b/src/mine_loop.rs index ff2efa4a..2224f1bc 100644 --- a/src/mine_loop.rs +++ b/src/mine_loop.rs @@ -311,6 +311,13 @@ fn create_block_transaction( .iter() .fold(NeptuneCoins::zero(), |acc, tx| acc + tx.kernel.fee); + // note: it is Ok to always use the same key here because: + // 1. if we find a block, the utxo will go to our wallet + // and notification occurs offchain, so there is no privacy issue. + // 2. if we were to derive a new addr for each block then we would + // have large gaps since an address only receives funds when + // we actually win the mining lottery. + // 3. also this way we do not have to modify global/wallet state. let coinbase_recipient_spending_key = global_state .wallet_state .wallet_secret @@ -324,7 +331,7 @@ fn create_block_transaction( let (coinbase_transaction, coinbase_sender_randomness) = make_coinbase_transaction( &coinbase_utxo, - receiving_address.privacy_digest, + receiving_address.privacy_digest(), &global_state.wallet_state.wallet_secret, next_block_height, latest_block.kernel.body.mutator_set_accumulator.clone(), @@ -575,7 +582,7 @@ mod mine_loop_tests { }; let (tx_by_preminer, expected_utxos) = premine_receiver_global_state .create_transaction_test_wrapper( - vec![TxOutput::fake_announcement( + vec![TxOutput::fake_address( tx_output, sender_randomness, receiver_privacy_digest, diff --git a/src/models/blockchain/block/block_selector.rs b/src/models/blockchain/block/block_selector.rs index cd2cb0a2..5a29fd23 100644 --- a/src/models/blockchain/block/block_selector.rs +++ b/src/models/blockchain/block/block_selector.rs @@ -32,7 +32,7 @@ pub enum BlockSelector { } /// BlockSelector can be written out as any of: -/// ``` +/// ```text /// genesis /// tip /// height/ diff --git a/src/models/blockchain/block/mod.rs b/src/models/blockchain/block/mod.rs index d00c6513..56f6e0ea 100644 --- a/src/models/blockchain/block/mod.rs +++ b/src/models/blockchain/block/mod.rs @@ -2,6 +2,7 @@ use crate::config_models::network::Network; use crate::models::consensus::mast_hash::MastHash; use crate::models::consensus::timestamp::Timestamp; use crate::models::consensus::{ValidityAstType, ValidityTree, WitnessType}; +use crate::models::state::wallet::address::ReceivingAddress; use crate::prelude::twenty_first; use get_size::GetSize; @@ -50,7 +51,6 @@ use super::transaction::Transaction; use super::type_scripts::neptune_coins::NeptuneCoins; use super::type_scripts::time_lock::TimeLock; use crate::models::blockchain::shared::Hash; -use crate::models::state::wallet::address::generation_address::{self, ReceivingAddress}; use crate::models::state::wallet::WalletSecret; use crate::util_types::mutator_set::commit; use crate::util_types::mutator_set::mutator_set_accumulator::MutatorSetAccumulator; @@ -285,7 +285,7 @@ impl Block { // generate randomness for mutator set commitment // Sender randomness cannot be random because there is no sender. let bad_randomness = Digest::default(); - let receiver_digest = receiving_address.privacy_digest; + let receiver_digest = receiving_address.privacy_digest(); // Add pre-mine UTXO to MutatorSet let addition_record = commit(utxo_digest, bad_randomness, receiver_digest); @@ -324,13 +324,13 @@ impl Block { Self::new(header, body, BlockType::Genesis) } - fn premine_distribution( - _network: Network, - ) -> Vec<(generation_address::ReceivingAddress, NeptuneCoins)> { + fn premine_distribution(_network: Network) -> Vec<(ReceivingAddress, NeptuneCoins)> { // The premine UTXOs can be hardcoded here. let authority_wallet = WalletSecret::devnet_wallet(); - let authority_receiving_address = - authority_wallet.nth_generation_spending_key(0).to_address(); + let authority_receiving_address = authority_wallet + .nth_generation_spending_key(0) + .to_address() + .into(); vec![ // chiefly for testing; anyone can access these coins by generating the devnet wallet as above (authority_receiving_address, NeptuneCoins::new(20000)), @@ -735,11 +735,11 @@ mod block_tests { .await .wallet_state .wallet_secret - .nth_generation_spending_key(0); + .nth_generation_spending_key_for_tests(0); let address = spending_key.to_address(); let other_wallet_secret = WalletSecret::new_random(); let other_address = other_wallet_secret - .nth_generation_spending_key(0) + .nth_generation_spending_key_for_tests(0) .to_address(); let genesis_block = Block::genesis_block(network); @@ -754,7 +754,7 @@ mod block_tests { // create a new transaction, merge it into block 1 and check that block 1 is still valid let new_utxo = Utxo::new_native_coin(other_address.lock_script(), NeptuneCoins::new(10)); let reciever_data = - TxOutput::fake_announcement(new_utxo, random(), other_address.privacy_digest); + TxOutput::fake_address(new_utxo, random(), other_address.privacy_digest); let (new_tx, expected_utxos) = global_state_lock .lock_guard() .await @@ -791,7 +791,9 @@ mod block_tests { let network = Network::RegTest; let a_wallet_secret = WalletSecret::new_random(); - let a_recipient_address = a_wallet_secret.nth_generation_spending_key(0).to_address(); + let a_recipient_address = a_wallet_secret + .nth_generation_spending_key_for_tests(0) + .to_address(); for multiplier in [10, 100, 1000, 10000, 100000, 1000000] { let mut block_prev = Block::genesis_block(network); @@ -889,7 +891,9 @@ mod block_tests { let genesis_block = Block::genesis_block(network); let a_wallet_secret = WalletSecret::new_random(); - let a_recipient_address = a_wallet_secret.nth_generation_spending_key(0).to_address(); + let a_recipient_address = a_wallet_secret + .nth_generation_spending_key_for_tests(0) + .to_address(); let (mut block_1, _, _) = make_mock_block_with_valid_pow(&genesis_block, None, a_recipient_address, rng.gen()); @@ -908,7 +912,9 @@ mod block_tests { let mut now = genesis_block.kernel.header.timestamp; let a_wallet_secret = WalletSecret::new_random(); - let a_recipient_address = a_wallet_secret.nth_generation_spending_key(0).to_address(); + let a_recipient_address = a_wallet_secret + .nth_generation_spending_key_for_tests(0) + .to_address(); let (mut block_1, _, _) = make_mock_block_with_valid_pow(&genesis_block, None, a_recipient_address, rng.gen()); @@ -953,7 +959,9 @@ mod block_tests { for i in 0..55 { let wallet_secret = WalletSecret::new_random(); - let recipient_address = wallet_secret.nth_generation_spending_key(0).to_address(); + let recipient_address = wallet_secret + .nth_generation_spending_key_for_tests(0) + .to_address(); let (new_block, _, _) = make_mock_block(blocks.last().unwrap(), None, recipient_address, rng.gen()); if i != 54 { @@ -1084,7 +1092,7 @@ mod block_tests { .await .wallet_state .wallet_secret - .nth_generation_spending_key(0); + .nth_generation_spending_key_for_tests(0); let address = spending_key.to_address(); let mut rng = thread_rng(); diff --git a/src/models/blockchain/transaction/mod.rs b/src/models/blockchain/transaction/mod.rs index cc81060e..c8ee2ffd 100644 --- a/src/models/blockchain/transaction/mod.rs +++ b/src/models/blockchain/transaction/mod.rs @@ -1,6 +1,9 @@ +//! implements [Transaction] and some types it depends on. + use crate::models::blockchain::block::mutator_set_update::MutatorSetUpdate; use crate::models::consensus::mast_hash::MastHash; use crate::models::consensus::{ValidityTree, WitnessType}; +use crate::models::state::wallet::utxo_notification_pool::ExpectedUtxo; use crate::prelude::{triton_vm, twenty_first}; pub mod primitive_witness; @@ -27,6 +30,7 @@ use triton_vm::prelude::NonDeterminism; use twenty_first::math::b_field_element::BFieldElement; use twenty_first::math::bfield_codec::BFieldCodec; use twenty_first::util_types::algebraic_hasher::AlgebraicHasher; +use utxo::Utxo; use self::primitive_witness::PrimitiveWitness; use self::transaction_kernel::TransactionKernel; @@ -46,6 +50,39 @@ pub use transaction_output::TxOutputList; pub use transaction_output::UtxoNotification; pub use transaction_output::UtxoNotifyMethod; +/// represents a utxo and secrets necessary for recipient to claim it. +/// +/// these are built from one of: +/// onchain symmetric-key public announcements +/// onchain asymmetric-key public announcements +/// offchain expected-utxos +/// +/// See [PublicAnnouncement], [UtxoNotification], [ExpectedUtxo] +#[derive(Clone, Debug)] +pub struct AnnouncedUtxo { + pub addition_record: AdditionRecord, + pub utxo: Utxo, + pub sender_randomness: Digest, + pub receiver_preimage: Digest, +} + +impl From<&ExpectedUtxo> for AnnouncedUtxo { + fn from(eu: &ExpectedUtxo) -> Self { + Self { + addition_record: eu.addition_record, + utxo: eu.utxo.clone(), + sender_randomness: eu.sender_randomness, + receiver_preimage: eu.receiver_preimage, + } + } +} + +/// represents arbitrary data that can be stored in a transaction on the public blockchain +/// +/// initially these are used for transmitting encrypted secrets necessary +/// for a utxo recipient to identify and claim it. +/// +/// See [Transaction], [UtxoNotification] #[derive( Clone, Debug, Serialize, Deserialize, PartialEq, Eq, GetSize, BFieldCodec, Default, Arbitrary, )] @@ -59,6 +96,7 @@ impl PublicAnnouncement { } } +/// represents a movement of [Utxo] on the blockchain #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, GetSize, BFieldCodec)] pub struct Transaction { pub kernel: TransactionKernel, diff --git a/src/models/blockchain/transaction/primitive_witness.rs b/src/models/blockchain/transaction/primitive_witness.rs index cc4e288a..627c28f5 100644 --- a/src/models/blockchain/transaction/primitive_witness.rs +++ b/src/models/blockchain/transaction/primitive_witness.rs @@ -104,7 +104,9 @@ impl PrimitiveWitness { ) -> (Vec, Vec, Vec>) { let input_spending_keys = address_seeds .iter() - .map(|address_seed| generation_address::SpendingKey::derive_from_seed(*address_seed)) + .map(|address_seed| { + generation_address::GenerationSpendingKey::derive_from_seed(*address_seed) + }) .collect_vec(); let input_lock_scripts = input_spending_keys .iter() @@ -172,7 +174,7 @@ impl PrimitiveWitness { .zip(output_amounts) .map(|(seed, amount)| { Utxo::new( - generation_address::SpendingKey::derive_from_seed(*seed) + generation_address::GenerationSpendingKey::derive_from_seed(*seed) .to_address() .lock_script(), amount.to_native_coins(), diff --git a/src/models/blockchain/transaction/transaction_input.rs b/src/models/blockchain/transaction/transaction_input.rs index 8089963f..5192e5a0 100644 --- a/src/models/blockchain/transaction/transaction_input.rs +++ b/src/models/blockchain/transaction/transaction_input.rs @@ -1,8 +1,10 @@ +//! provides an interface to transaction inputs + use super::utxo::LockScript; use super::utxo::Utxo; use crate::models::blockchain::shared::Hash; use crate::models::blockchain::type_scripts::neptune_coins::NeptuneCoins; -use crate::models::state::wallet::address::SpendingKeyType; +use crate::models::state::wallet::address::SpendingKey; use crate::util_types::mutator_set::ms_membership_proof::MsMembershipProof; use crate::util_types::mutator_set::mutator_set_accumulator::MutatorSetAccumulator; use crate::util_types::mutator_set::removal_record::RemovalRecord; @@ -11,10 +13,10 @@ use std::ops::DerefMut; use tasm_lib::twenty_first::prelude::AlgebraicHasher; /// represents a transaction input, as accepted by -/// `GlobalState::create_transaction()` +/// [create_transaction()](crate::models::state::GlobalState::create_transaction()) #[derive(Debug, Clone)] pub struct TxInput { - pub spending_key: SpendingKeyType, + pub spending_key: SpendingKey, pub utxo: Utxo, pub lock_script: LockScript, pub ms_membership_proof: MsMembershipProof, @@ -99,12 +101,12 @@ impl TxInputList { } /// retrieves spending keys - pub fn spending_keys_iter(&self) -> impl IntoIterator + '_ { + pub fn spending_keys_iter(&self) -> impl IntoIterator + '_ { self.0.iter().map(|u| u.spending_key) } /// retrieves spending keys - pub fn spending_keys(&self) -> Vec { + pub fn spending_keys(&self) -> Vec { self.spending_keys_iter().into_iter().collect() } diff --git a/src/models/blockchain/transaction/transaction_output.rs b/src/models/blockchain/transaction/transaction_output.rs index d6559e1f..ad556675 100644 --- a/src/models/blockchain/transaction/transaction_output.rs +++ b/src/models/blockchain/transaction/transaction_output.rs @@ -1,8 +1,11 @@ +//! provides an interface to transaction outputs and associated types + use crate::models::blockchain::shared::Hash; use crate::models::blockchain::transaction::utxo::Utxo; use crate::models::blockchain::transaction::PublicAnnouncement; use crate::models::blockchain::type_scripts::neptune_coins::NeptuneCoins; -use crate::models::state::wallet::address::ReceivingAddressType; +use crate::models::state::wallet::address::ReceivingAddress; +use crate::models::state::wallet::address::SpendingKey; use crate::models::state::wallet::utxo_notification_pool::ExpectedUtxo; use crate::models::state::wallet::utxo_notification_pool::UtxoNotifier; use crate::models::state::wallet::wallet_state::WalletState; @@ -11,33 +14,54 @@ use crate::prelude::twenty_first::util_types::algebraic_hasher::AlgebraicHasher; use crate::util_types::mutator_set::addition_record::AdditionRecord; use crate::util_types::mutator_set::commit; use anyhow::Result; +use serde::Deserialize; +use serde::Serialize; use std::ops::Deref; use std::ops::DerefMut; -/// enumerates how a transaction recipient should be notified -/// that a Utxo exists which they can claim/spend. +/// enumerates how utxos should be transferred. /// /// see also: [UtxoNotification] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub enum UtxoNotifyMethod { - OnChainPubKey, - OnChainSymmetricKey, + /// the utxo notification should be transferred to recipient encrypted on the blockchain + OnChain, + + /// the utxo notification should be transferred to recipient off the blockchain OffChain, } -/// enumerates transaction notifications. +/// enumerates utxo transfer methods with payloads +/// +/// [PublicAnnouncement] is essentially opaque however one can determine the key +/// type via [`KeyType::try_from::()`](crate::models::state::wallet::address::KeyType::try_from::()) +/// +/// see also: [UtxoNotifyMethod], [KeyType](crate::models::state::wallet::address::KeyType) +/// +/// future work: +/// +/// we should consider adding this variant that would facilitate passing +/// utxo from sender to receiver off-chain for lower-fee transfers between +/// trusted parties or eg wallets owned by the same person/org. +/// +/// OffChainSerialized(PublicAnnouncement) +/// +/// also, perhaps PublicAnnouncement should be used for `OffChain` +/// and replace ExpectedUtxo. to consolidate code/logic. +/// +/// see comment for: [TxOutput::auto()] /// -/// This mirrors variants in [`UtxoNotifyMethod`] but also holds notification -/// data. #[derive(Debug, Clone)] pub enum UtxoNotification { - OnChainPubKey(PublicAnnouncement), - OnChainSymmetricKey(PublicAnnouncement), + /// the utxo notification should be transferred to recipient on the blockchain as a [PublicAnnouncement] + OnChain(PublicAnnouncement), + + /// the utxo notification should be transferred to recipient off the blockchain as an [ExpectedUtxo] OffChain(Box), } /// represents a transaction output, as accepted by -/// `GlobalState::create_transaction()` +/// [GlobalState::create_transaction()](crate::models::state::GlobalState::create_transaction()) /// /// Contains data that a UTXO recipient requires in order to be notified about /// and claim a given UTXO @@ -72,53 +96,89 @@ impl From<&TxOutput> for AdditionRecord { } impl TxOutput { - /// automatically generates `TxOutput` from address and amount. + /// automatically generates [TxOutput] using some heuristics + /// + /// If the [Utxo] cannot be claimed by our wallet then `OnChain` transfer + /// will be used. A [PublicAnnouncement] will be created using whichever + /// address type is provided. + /// + /// If the [Utxo] can be claimed by our wallet, then + /// `owned_utxo_notify_method` dictates the behavior: + /// + /// * `OffChain` results in local state transfer via whichever address type is provided. + /// * `OnChain` results in blockchain transfer via whichever address type is provided. /// - /// If the `Utxo` can be claimed by our wallet then private OffChain - /// notification will be used. Else `OnChain` notification. + /// design decision: we do not return any error if a pub-key is used for + /// onchain notification of an owned utxo. /// - /// This method should normally be used when instantiating `TxOutput` + /// rationale: this is not an intended use case, however: + /// 1. this keeps the logic simple and straight-forward. most important. + /// 2. we can't truly stop it, people could always modify the software. + /// 3. neptune-core wallet(s) will not generate that condition. + /// 4. users are incentivized not to do this as it uses more blockchain space + /// and thus they will have a higher fee. + /// + /// design decision: we do not return any error if a symmetric-key is used + /// for sending outside this wallet. + /// + /// rationale: this is not an intended use case, however: + /// 1. this keeps the logic simple and straight-forward. most important. + /// 2. we can't truly stop it, people could always modify the software. + /// 3. neptune-core wallet(s) will not generate that condition. + /// 4. valid use-cases exist like sending between two wallets that + /// are owned by the same owner or family members. In this case + /// the user knows more than the software about what is "safe". + /// 5. why make an API that limits power users? + /// + /// future work: + /// + /// accept param `unowned_utxo_notify_method` that would specify `OnChain` + /// or `OffChain` behavior for un-owned utxos. This would facilitate + /// off-chain notifications and lower tx fees between wallets controlled by + /// the same person/org, or even untrusted 3rd parties when receiver uses an + /// optional resend-to-self feature when claiming. /// - /// note: in the future, OnChainSymmetric may be preferred instead of - /// OffChain for `Utxo` that can be claimed by our wallet. pub fn auto( wallet_state: &WalletState, - address: &ReceivingAddressType, + address: &ReceivingAddress, amount: NeptuneCoins, sender_randomness: Digest, + owned_utxo_notify_method: UtxoNotifyMethod, ) -> Result { + let onchain = || -> Result { + let utxo = Utxo::new_native_coin(address.lock_script(), amount); + let pub_ann = address.generate_public_announcement(&utxo, sender_randomness)?; + Ok(Self::onchain( + utxo, + sender_randomness, + address.privacy_digest(), + pub_ann, + )) + }; + + let offchain = |key: SpendingKey| { + let utxo = Utxo::new_native_coin(address.lock_script(), amount); + Self::offchain(utxo, sender_randomness, key.privacy_preimage()) + }; + let utxo = Utxo::new_native_coin(address.lock_script(), amount); + let utxo_wallet_key = wallet_state.find_spending_key_for_utxo(&utxo); - Ok(match wallet_state.find_spending_key_for_utxo(&utxo) { - Some(key) => Self::offchain(utxo, sender_randomness, key.privacy_preimage()), - None => { - let pub_ann = address.generate_public_announcement(&utxo, sender_randomness)?; - Self::onchain_pubkey(utxo, sender_randomness, address.privacy_digest(), pub_ann) - } - }) - } + let tx_output = match utxo_wallet_key { + None => onchain()?, + Some(key) => match owned_utxo_notify_method { + UtxoNotifyMethod::OnChain => onchain()?, + UtxoNotifyMethod::OffChain => offchain(key), + }, + }; - /// instantiates `TxOutput` using OnChainPubKey notification method. - /// - /// For normal situations, auto() should be used instead. - pub fn onchain_pubkey( - utxo: Utxo, - sender_randomness: Digest, - receiver_privacy_digest: Digest, - public_announcement: PublicAnnouncement, - ) -> Self { - Self { - utxo, - sender_randomness, - receiver_privacy_digest, - utxo_notification: UtxoNotification::OnChainPubKey(public_announcement), - } + Ok(tx_output) } - /// instantiates `TxOutput` using OnChainSymmetricKey notification method. + /// instantiates `TxOutput` using `OnChain` transfer method. /// /// For normal situations, auto() should be used instead. - pub fn onchain_symkey( + pub fn onchain( utxo: Utxo, sender_randomness: Digest, receiver_privacy_digest: Digest, @@ -128,11 +188,11 @@ impl TxOutput { utxo, sender_randomness, receiver_privacy_digest, - utxo_notification: UtxoNotification::OnChainSymmetricKey(public_announcement), + utxo_notification: UtxoNotification::OnChain(public_announcement), } } - /// instantiates `TxOutput` using OffChain notification method. + /// instantiates `TxOutput` using `OffChain` transfer method. /// /// For normal situations, auto() should be used instead. pub fn offchain( @@ -149,25 +209,33 @@ impl TxOutput { .into() } - // only for tests + // only for legacy tests #[cfg(test)] - pub fn fake_announcement( + pub fn fake_address( utxo: Utxo, sender_randomness: Digest, receiver_privacy_digest: Digest, ) -> Self { + use crate::models::state::wallet::address::generation_address::GenerationReceivingAddress; + + let address: ReceivingAddress = + GenerationReceivingAddress::derive_from_seed(rand::random()).into(); + let announcement = address + .generate_public_announcement(&utxo, sender_randomness) + .unwrap(); + Self { utxo, sender_randomness, receiver_privacy_digest, - utxo_notification: UtxoNotification::OnChainPubKey(Default::default()), + utxo_notification: UtxoNotification::OnChain(announcement), } } - // only for tests + // only for legacy tests #[cfg(test)] pub fn random(utxo: Utxo) -> Self { - Self::fake_announcement(utxo, rand::random(), rand::random()) + Self::fake_address(utxo, rand::random(), rand::random()) } } @@ -220,6 +288,7 @@ impl From<&TxOutputList> for Vec { } impl TxOutputList { + /// calculates total amount in native currency pub fn total_native_coins(&self) -> NeptuneCoins { self.0 .iter() @@ -250,8 +319,7 @@ impl TxOutputList { /// retrieves public announcements from possible sub-set of the list pub fn public_announcements_iter(&self) -> impl IntoIterator + '_ { self.0.iter().filter_map(|u| match &u.utxo_notification { - UtxoNotification::OnChainPubKey(pa) => Some(pa.clone()), - UtxoNotification::OnChainSymmetricKey(pa) => Some(pa.clone()), + UtxoNotification::OnChain(pa) => Some(pa.clone()), _ => None, }) } @@ -269,6 +337,13 @@ impl TxOutputList { }) } + /// indicates if any offchain notifications (ExpectedUtxo) exist + pub fn has_offchain(&self) -> bool { + self.0 + .iter() + .any(|u| matches!(&u.utxo_notification, UtxoNotification::OffChain(_))) + } + /// retrieves expected_utxos from possible sub-set of the list pub fn expected_utxos(&self) -> Vec { self.expected_utxos_iter().into_iter().collect() @@ -280,13 +355,14 @@ mod tests { use super::*; use crate::config_models::network::Network; use crate::models::blockchain::type_scripts::neptune_coins::NeptuneCoins; - use crate::models::state::wallet::address::generation_address::ReceivingAddress; + use crate::models::state::wallet::address::generation_address::GenerationReceivingAddress; + use crate::models::state::wallet::address::KeyType; use crate::models::state::wallet::WalletSecret; use crate::tests::shared::mock_genesis_global_state; use rand::Rng; #[tokio::test] - async fn test_utxoreceiver_auto_on_chain_pubkey() -> Result<()> { + async fn test_utxoreceiver_auto_not_owned_output() -> Result<()> { let global_state_lock = mock_genesis_global_state(Network::RegTest, 2, WalletSecret::devnet_wallet()).await; @@ -296,7 +372,7 @@ mod tests { // generate a new receiving address that is not from our wallet. let mut rng = rand::thread_rng(); let seed: Digest = rng.gen(); - let address = ReceivingAddress::derive_from_seed(seed); + let address = GenerationReceivingAddress::derive_from_seed(seed); let amount = NeptuneCoins::one(); let utxo = Utxo::new_native_coin(address.lock_script(), amount); @@ -306,68 +382,98 @@ mod tests { .wallet_secret .generate_sender_randomness(block_height, address.privacy_digest); - let utxo_receiver = TxOutput::auto( - &state.wallet_state, - &address.into(), - amount, - sender_randomness, - )?; - - assert!(matches!( - utxo_receiver.utxo_notification, - UtxoNotification::OnChainPubKey(_) - )); - assert_eq!(utxo_receiver.sender_randomness, sender_randomness); - assert_eq!( - utxo_receiver.receiver_privacy_digest, - address.privacy_digest - ); - assert_eq!(utxo_receiver.utxo, utxo); + for utxo_notify_method in [UtxoNotifyMethod::OffChain, UtxoNotifyMethod::OnChain] { + let utxo_receiver = TxOutput::auto( + &state.wallet_state, + &address.into(), + amount, + sender_randomness, + utxo_notify_method, // how to notify of owned utxos. + )?; + + // we should have OnChain transfer regardless of owned_transfer_method setting + // because it only applies to owned outputs. + assert!(matches!( + utxo_receiver.utxo_notification, + UtxoNotification::OnChain(_) + )); + assert_eq!(utxo_receiver.sender_randomness, sender_randomness); + assert_eq!( + utxo_receiver.receiver_privacy_digest, + address.privacy_digest + ); + assert_eq!(utxo_receiver.utxo, utxo); + } + Ok(()) } #[tokio::test] - async fn test_utxoreceiver_auto_off_chain() -> Result<()> { + async fn test_utxoreceiver_auto_owned_output() -> Result<()> { let global_state_lock = mock_genesis_global_state(Network::RegTest, 2, WalletSecret::devnet_wallet()).await; // obtain next unused receiving address from our wallet. - let spending_key = global_state_lock + let spending_key_gen = global_state_lock .lock_guard_mut() .await .wallet_state - .wallet_secret - .next_unused_generation_spending_key(); - let address = spending_key.to_address(); + .next_unused_spending_key(KeyType::Generation); + let address_gen = spending_key_gen.to_address(); + + // obtain next unused symmetric address from our wallet. + let spending_key_sym = global_state_lock + .lock_guard_mut() + .await + .wallet_state + .next_unused_spending_key(KeyType::Symmetric); + let address_sym = spending_key_sym.to_address(); let state = global_state_lock.lock_guard().await; let block_height = state.chain.light_state().header().height; let amount = NeptuneCoins::one(); - let utxo = Utxo::new_native_coin(address.lock_script(), amount); - let sender_randomness = state - .wallet_state - .wallet_secret - .generate_sender_randomness(block_height, address.privacy_digest); + for (transfer_method, address) in [ + (UtxoNotifyMethod::OffChain, address_gen.clone()), + (UtxoNotifyMethod::OnChain, address_sym.clone()), + ] { + let utxo = Utxo::new_native_coin(address.lock_script(), amount); + let sender_randomness = state + .wallet_state + .wallet_secret + .generate_sender_randomness(block_height, address.privacy_digest()); + + let utxo_receiver = TxOutput::auto( + &state.wallet_state, + &address, + amount, + sender_randomness, + transfer_method, // how to notify of owned utxos. + )?; + + let transfer_is_correct = match utxo_receiver.utxo_notification { + UtxoNotification::OffChain(_) => { + matches!(transfer_method, UtxoNotifyMethod::OffChain) + } + UtxoNotification::OnChain(ref pa) => match transfer_method { + UtxoNotifyMethod::OnChain => address.matches_public_announcement_key_type(pa), + _ => false, + }, + }; + + println!("owned_transfer_method: {:#?}", transfer_method); + println!("utxo_transfer: {:#?}", utxo_receiver.utxo_notification); + + assert!(transfer_is_correct); + assert_eq!(utxo_receiver.sender_randomness, sender_randomness); + assert_eq!( + utxo_receiver.receiver_privacy_digest, + address.privacy_digest() + ); + assert_eq!(utxo_receiver.utxo, utxo); + } - let utxo_receiver = TxOutput::auto( - &state.wallet_state, - &address.into(), - amount, - sender_randomness, - )?; - - assert!(matches!( - utxo_receiver.utxo_notification, - UtxoNotification::OffChain(_) - )); - assert_eq!(utxo_receiver.sender_randomness, sender_randomness); - assert_eq!( - utxo_receiver.receiver_privacy_digest, - address.privacy_digest - ); - assert_eq!(utxo_receiver.utxo, utxo); Ok(()) } } diff --git a/src/models/blockchain/type_scripts/neptune_coins.rs b/src/models/blockchain/type_scripts/neptune_coins.rs index 152e9359..84a3ab59 100644 --- a/src/models/blockchain/type_scripts/neptune_coins.rs +++ b/src/models/blockchain/type_scripts/neptune_coins.rs @@ -36,7 +36,7 @@ use tasm_lib::{structure::tasm_object::TasmObject, twenty_first::math::bfield_co /// program related to block validity, it is important to use `safe_add` rather than `+` as /// the latter operation does not care about overflow. Not testing for overflow can cause /// inflation bugs. -#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, BFieldCodec, TasmObject)] +#[derive(Clone, Copy, Serialize, Deserialize, Eq, BFieldCodec, TasmObject)] pub struct NeptuneCoins(u128); impl NeptuneCoins { @@ -241,6 +241,24 @@ impl Zero for NeptuneCoins { } } +impl From for NeptuneCoins { + fn from(n: u32) -> NeptuneCoins { + Self::new(n) + } +} + +impl From for NeptuneCoins { + fn from(n: u16) -> NeptuneCoins { + Self::new(n as u32) + } +} + +impl From for NeptuneCoins { + fn from(n: u8) -> NeptuneCoins { + Self::new(n as u32) + } +} + impl FromStr for NeptuneCoins { type Err = anyhow::Error; @@ -319,6 +337,14 @@ impl Display for NeptuneCoins { } } +impl std::fmt::Debug for NeptuneCoins { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("NeptuneCoins") + .field(&self.to_string()) + .finish() + } +} + pub fn pseudorandom_amount(seed: [u8; 32]) -> NeptuneCoins { let mut rng: StdRng = SeedableRng::from_seed(seed); let number: u128 = rng.gen::() >> 10; diff --git a/src/models/state/archival_state.rs b/src/models/state/archival_state.rs index f6d50c1b..1277352f 100644 --- a/src/models/state/archival_state.rs +++ b/src/models/state/archival_state.rs @@ -878,7 +878,7 @@ mod archival_state_tests { let b = Block::genesis_block(network); let some_wallet_secret = WalletSecret::new_random(); - let some_spending_key = some_wallet_secret.nth_generation_spending_key(0); + let some_spending_key = some_wallet_secret.nth_generation_spending_key_for_tests(0); let some_receiving_address = some_spending_key.to_address(); let (block_1, _, _) = @@ -942,7 +942,7 @@ mod archival_state_tests { None, genesis_wallet_state .wallet_secret - .nth_generation_spending_key(0) + .nth_generation_spending_key_for_tests(0) .to_address(), rng.gen(), ); @@ -976,7 +976,7 @@ mod archival_state_tests { let genesis_wallet_state = mock_genesis_wallet_state(WalletSecret::devnet_wallet(), network).await; let wallet = genesis_wallet_state.wallet_secret; - let own_receiving_address = wallet.nth_generation_spending_key(0).to_address(); + let own_receiving_address = wallet.nth_generation_spending_key_for_tests(0).to_address(); let genesis_receiver_global_state_lock = mock_genesis_global_state(network, 0, wallet).await; let mut genesis_receiver_global_state = @@ -1064,7 +1064,9 @@ mod archival_state_tests { let (mut archival_state, _peer_db_lock, _data_dir) = mock_genesis_archival_state(network).await; let own_wallet = WalletSecret::new_random(); - let own_receiving_address = own_wallet.nth_generation_spending_key(0).to_address(); + let own_receiving_address = own_wallet + .nth_generation_spending_key_for_tests(0) + .to_address(); // 1. Create new block 1 and store it to the DB let (mock_block_1a, _, _) = make_mock_block_with_valid_pow( @@ -1115,7 +1117,9 @@ mod archival_state_tests { let genesis_wallet_state = mock_genesis_wallet_state(WalletSecret::devnet_wallet(), network).await; let genesis_wallet = genesis_wallet_state.wallet_secret; - let own_receiving_address = genesis_wallet.nth_generation_spending_key(0).to_address(); + let own_receiving_address = genesis_wallet + .nth_generation_spending_key_for_tests(0) + .to_address(); let global_state_lock = mock_genesis_global_state(Network::RegTest, 42, genesis_wallet).await; let mut num_utxos = Block::premine_utxos(network).len(); @@ -1234,7 +1238,9 @@ mod archival_state_tests { let genesis_wallet_state = mock_genesis_wallet_state(WalletSecret::devnet_wallet(), network).await; let genesis_wallet = genesis_wallet_state.wallet_secret; - let own_receiving_address = genesis_wallet.nth_generation_spending_key(0).to_address(); + let own_receiving_address = genesis_wallet + .nth_generation_spending_key_for_tests(0) + .to_address(); let global_state_lock = mock_genesis_global_state(Network::RegTest, 42, genesis_wallet).await; @@ -1401,7 +1407,9 @@ mod archival_state_tests { let genesis_wallet_state = mock_genesis_wallet_state(WalletSecret::devnet_wallet(), network).await; let genesis_wallet = genesis_wallet_state.wallet_secret; - let own_receiving_address = genesis_wallet.nth_generation_spending_key(0).to_address(); + let own_receiving_address = genesis_wallet + .nth_generation_spending_key_for_tests(0) + .to_address(); let genesis_block = Block::genesis_block(network); let now = genesis_block.kernel.header.timestamp; let seven_months = Timestamp::months(7); @@ -1458,16 +1466,16 @@ mod archival_state_tests { mock_genesis_wallet_state(WalletSecret::devnet_wallet(), network).await; let genesis_spending_key = genesis_wallet_state .wallet_secret - .nth_generation_spending_key(0); + .nth_generation_spending_key_for_tests(0); let genesis_state_lock = mock_genesis_global_state(network, 3, genesis_wallet_state.wallet_secret).await; let wallet_secret_alice = WalletSecret::new_random(); - let alice_spending_key = wallet_secret_alice.nth_generation_spending_key(0); + let alice_spending_key = wallet_secret_alice.nth_generation_spending_key_for_tests(0); let alice_state_lock = mock_genesis_global_state(network, 3, wallet_secret_alice).await; let wallet_secret_bob = WalletSecret::new_random(); - let bob_spending_key = wallet_secret_bob.nth_generation_spending_key(0); + let bob_spending_key = wallet_secret_bob.nth_generation_spending_key_for_tests(0); let bob_state_lock = mock_genesis_global_state(network, 3, wallet_secret_bob).await; let genesis_block = Block::genesis_block(network); @@ -1485,7 +1493,7 @@ mod archival_state_tests { let fee = NeptuneCoins::one(); let sender_randomness: Digest = random(); let tx_outputs_for_alice = vec![ - TxOutput::fake_announcement( + TxOutput::fake_address( Utxo { lock_script_hash: alice_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(41).to_native_coins(), @@ -1493,7 +1501,7 @@ mod archival_state_tests { sender_randomness, alice_spending_key.to_address().privacy_digest, ), - TxOutput::fake_announcement( + TxOutput::fake_address( Utxo { lock_script_hash: alice_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(59).to_native_coins(), @@ -1504,7 +1512,7 @@ mod archival_state_tests { ]; // Two outputs for Bob let tx_outputs_for_bob = vec![ - TxOutput::fake_announcement( + TxOutput::fake_address( Utxo { lock_script_hash: bob_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(141).to_native_coins(), @@ -1512,7 +1520,7 @@ mod archival_state_tests { sender_randomness, bob_spending_key.to_address().privacy_digest, ), - TxOutput::fake_announcement( + TxOutput::fake_address( Utxo { lock_script_hash: bob_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(59).to_native_coins(), @@ -1646,7 +1654,7 @@ mod archival_state_tests { // Make two transactions: Alice sends two UTXOs to Genesis (50 + 49 coins and 1 in fee) // and Bob sends three UTXOs to genesis (50 + 50 + 98 and 2 in fee) let tx_outputs_from_alice = vec![ - TxOutput::fake_announcement( + TxOutput::fake_address( Utxo { lock_script_hash: genesis_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(50).to_native_coins(), @@ -1654,7 +1662,7 @@ mod archival_state_tests { random(), genesis_spending_key.to_address().privacy_digest, ), - TxOutput::fake_announcement( + TxOutput::fake_address( Utxo { lock_script_hash: genesis_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(49).to_native_coins(), @@ -1683,7 +1691,7 @@ mod archival_state_tests { .unwrap(); let tx_outputs_from_bob = vec![ - TxOutput::fake_announcement( + TxOutput::fake_address( Utxo { lock_script_hash: genesis_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(50).to_native_coins(), @@ -1691,7 +1699,7 @@ mod archival_state_tests { random(), genesis_spending_key.to_address().privacy_digest, ), - TxOutput::fake_announcement( + TxOutput::fake_address( Utxo { lock_script_hash: genesis_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(50).to_native_coins(), @@ -1699,7 +1707,7 @@ mod archival_state_tests { random(), genesis_spending_key.to_address().privacy_digest, ), - TxOutput::fake_announcement( + TxOutput::fake_address( Utxo { lock_script_hash: genesis_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(98).to_native_coins(), @@ -1881,7 +1889,9 @@ mod archival_state_tests { // Add a block to archival state and verify that this is returned let mut rng = thread_rng(); let own_wallet = WalletSecret::new_random(); - let own_receiving_address = own_wallet.nth_generation_spending_key(0).to_address(); + let own_receiving_address = own_wallet + .nth_generation_spending_key_for_tests(0) + .to_address(); let genesis = *archival_state.genesis_block.clone(); let (mock_block_1, _, _) = make_mock_block_with_valid_pow(&genesis, None, own_receiving_address, rng.gen()); @@ -1936,7 +1946,9 @@ mod archival_state_tests { let genesis = *archival_state.genesis_block.clone(); let own_wallet = WalletSecret::new_random(); - let own_receiving_address = own_wallet.nth_generation_spending_key(0).to_address(); + let own_receiving_address = own_wallet + .nth_generation_spending_key_for_tests(0) + .to_address(); let (mock_block_1, _, _) = make_mock_block_with_valid_pow( &genesis.clone(), None, @@ -2037,7 +2049,9 @@ mod archival_state_tests { // Add a fork with genesis as LUCA and verify that correct results are returned let own_wallet = WalletSecret::new_random(); - let own_receiving_address = own_wallet.nth_generation_spending_key(0).to_address(); + let own_receiving_address = own_wallet + .nth_generation_spending_key_for_tests(0) + .to_address(); let (mock_block_1_a, _, _) = make_mock_block_with_valid_pow( &genesis.clone(), None, @@ -2186,7 +2200,9 @@ mod archival_state_tests { // Insert a block that is descendant from genesis block and verify that it is canonical let own_wallet = WalletSecret::new_random(); - let own_receiving_address = own_wallet.nth_generation_spending_key(0).to_address(); + let own_receiving_address = own_wallet + .nth_generation_spending_key_for_tests(0) + .to_address(); let (mock_block_1, _, _) = make_mock_block_with_valid_pow( &genesis.clone(), None, @@ -2619,7 +2635,9 @@ mod archival_state_tests { let mut archival_state = make_test_archival_state(Network::Alpha).await; let genesis = *archival_state.genesis_block.clone(); let own_wallet = WalletSecret::new_random(); - let own_receiving_address = own_wallet.nth_generation_spending_key(0).to_address(); + let own_receiving_address = own_wallet + .nth_generation_spending_key_for_tests(0) + .to_address(); assert!(archival_state .get_ancestor_block_digests(genesis.hash(), 10) @@ -2735,7 +2753,9 @@ mod archival_state_tests { let mut archival_state = make_test_archival_state(Network::Alpha).await; let genesis = *archival_state.genesis_block.clone(); let own_wallet = WalletSecret::new_random(); - let own_receiving_address = own_wallet.nth_generation_spending_key(0).to_address(); + let own_receiving_address = own_wallet + .nth_generation_spending_key_for_tests(0) + .to_address(); let (mock_block_1, _, _) = make_mock_block_with_valid_pow( &genesis.clone(), diff --git a/src/models/state/mempool.rs b/src/models/state/mempool.rs index 019496ab..f1cc4191 100644 --- a/src/models/state/mempool.rs +++ b/src/models/state/mempool.rs @@ -621,14 +621,16 @@ mod tests { premine_receiver_global_state.lock_guard_mut().await; let premine_wallet_secret = &premine_receiver_global_state.wallet_state.wallet_secret; - let premine_receiver_spending_key = premine_wallet_secret.nth_generation_spending_key(0); + let premine_receiver_spending_key = + premine_wallet_secret.nth_generation_spending_key_for_tests(0); let premine_receiver_address = premine_receiver_spending_key.to_address(); let other_wallet_secret = WalletSecret::new_pseudorandom(rng.gen()); let other_global_state_lock = mock_genesis_global_state(network, 2, other_wallet_secret.clone()).await; let mut other_global_state = other_global_state_lock.lock_guard_mut().await; - let other_receiver_spending_key = other_wallet_secret.nth_generation_spending_key(0); + let other_receiver_spending_key = + other_wallet_secret.nth_generation_spending_key_for_tests(0); let other_receiver_address = other_receiver_spending_key.to_address(); // Ensure that both wallets have a non-zero balance @@ -665,7 +667,7 @@ mod tests { lock_script_hash: premine_receiver_address.lock_script().hash(), }; - output_utxos_generated_by_me.push(TxOutput::fake_announcement( + output_utxos_generated_by_me.push(TxOutput::fake_address( new_utxo, random(), premine_receiver_address.privacy_digest, @@ -697,7 +699,7 @@ mod tests { // not included in block 2 it must still be in the mempool after the mempool has been // updated with block 2. Also: The transaction must be valid after block 2 as the mempool // manager must keep mutator set data updated. - let output_utxo_data_by_miner = vec![TxOutput::fake_announcement( + let output_utxo_data_by_miner = vec![TxOutput::fake_address( Utxo { coins: NeptuneCoins::new(68).to_native_coins(), lock_script_hash: other_receiver_address.lock_script().hash(), @@ -833,19 +835,18 @@ mod tests { let premine_address = premine_receiver_global_state .wallet_state .wallet_secret - .nth_generation_spending_key(0) + .nth_generation_spending_key_for_tests(0) .to_address(); let other_wallet_secret = WalletSecret::new_random(); let other_address = other_wallet_secret - .nth_generation_spending_key(0) + .nth_generation_spending_key_for_tests(0) .to_address(); let utxo = Utxo::new( premine_address.lock_script(), NeptuneCoins::new(1).to_native_coins(), ); - let tx_tx_outputs = - TxOutput::fake_announcement(utxo, random(), premine_address.privacy_digest); + let tx_tx_outputs = TxOutput::fake_address(utxo, random(), premine_address.privacy_digest); let genesis_block = premine_receiver_global_state .chain @@ -966,7 +967,7 @@ mod tests { let seven_months = Timestamp::months(7); let mut preminer_state = preminer_state_lock.lock_guard_mut().await; let premine_wallet_secret = &preminer_state.wallet_state.wallet_secret; - let premine_spending_key = premine_wallet_secret.nth_generation_spending_key(0); + let premine_spending_key = premine_wallet_secret.nth_generation_spending_key_for_tests(0); let premine_address = premine_spending_key.to_address(); // Create a transaction and insert it into the mempool @@ -974,8 +975,7 @@ mod tests { coins: NeptuneCoins::new(1).to_native_coins(), lock_script_hash: premine_address.lock_script().hash(), }; - let tx_outputs = - TxOutput::fake_announcement(utxo, random(), premine_address.privacy_digest); + let tx_outputs = TxOutput::fake_address(utxo, random(), premine_address.privacy_digest); let (tx_by_preminer_low_fee, expected_utxos_low_fee) = preminer_state .create_transaction_test_wrapper( vec![tx_outputs.clone()], diff --git a/src/models/state/mod.rs b/src/models/state/mod.rs index 1cbe120d..271833c9 100644 --- a/src/models/state/mod.rs +++ b/src/models/state/mod.rs @@ -1,8 +1,7 @@ use self::blockchain_state::BlockchainState; use self::mempool::Mempool; use self::networking_state::NetworkingState; -use self::wallet::address::ReceivingAddressType; -use self::wallet::address::SpendingKeyType; +use self::wallet::address::ReceivingAddress; use self::wallet::utxo_notification_pool::UtxoNotifier; use self::wallet::wallet_state::WalletState; use self::wallet::wallet_status::WalletStatus; @@ -16,7 +15,6 @@ use super::blockchain::transaction::Transaction; use super::blockchain::transaction::TxInputList; use super::blockchain::transaction::TxOutput; use super::blockchain::transaction::TxOutputList; -use super::blockchain::transaction::UtxoNotifyMethod; use super::blockchain::type_scripts::native_currency::NativeCurrency; use super::blockchain::type_scripts::neptune_coins::NeptuneCoins; use super::blockchain::type_scripts::time_lock::TimeLock; @@ -28,6 +26,7 @@ use crate::database::storage::storage_schema::traits::StorageWriter as SW; use crate::database::storage::storage_vec::traits::*; use crate::database::storage::storage_vec::Index; use crate::locks::tokio as sync_tokio; +use crate::models::blockchain::transaction::UtxoNotifyMethod; use crate::models::peer::HandshakeData; use crate::models::state::wallet::monitored_utxo::MonitoredUtxo; use crate::models::state::wallet::utxo_notification_pool::ExpectedUtxo; @@ -43,6 +42,7 @@ use std::ops::{Deref, DerefMut}; use tracing::{debug, info, warn}; use twenty_first::math::digest::Digest; use twenty_first::util_types::algebraic_hasher::AlgebraicHasher; +use wallet::address::SpendingKey; pub mod archival_state; pub mod blockchain_state; @@ -154,6 +154,11 @@ impl GlobalStateLock { self.lock_mut(|s| s.mining = mining).await } + // persist wallet state to disk + pub async fn persist_wallet(&mut self) -> Result<()> { + self.lock_guard_mut().await.persist_wallet().await + } + // flush databases (persist to disk) pub async fn flush_databases(&self) -> Result<()> { self.lock_guard_mut().await.flush_databases().await @@ -414,50 +419,56 @@ impl GlobalState { } } - /// generates `TxOutputList` from a list of address:amount pairs (outputs). + /// generates [TxOutputList] from a list of address:amount pairs (outputs). /// /// This is a helper method for generating the `TxOutputList` that - /// is required by create_transaction() and create_raw_transaction(). + /// is required by [Self::create_transaction()] and [Self::create_raw_transaction()]. /// - /// For each output, if a wallet key exists for the address then OffChain - /// notification will be used via `ExpectedUtxo`. Otherwise OnChainPubKey - /// notification will be used via `PublicAnnouncement`. + /// Each output may use either `OnChain` or `OffChain` notifications. See documentation of + /// of [TxOutput::auto()] for a description of the logic and the + /// `owned_utxo_notify_method` parameter. /// /// If a different behavior is desired, the TxOutputList can be /// constructed manually. + /// + /// future work: + /// + /// see future work comment in [TxOutput::auto()] pub fn generate_tx_outputs( &self, - outputs: impl IntoIterator, + outputs: impl IntoIterator, + owned_utxo_notify_method: UtxoNotifyMethod, ) -> Result { - let mut tx_outputs = TxOutputList::default(); let block_height = self.chain.light_state().header().height; // Convert outputs. [address:amount] --> TxOutputList - for (address, amount) in outputs.into_iter() { - let sender_randomness = self - .wallet_state - .wallet_secret - .generate_sender_randomness(block_height, address.privacy_digest()); - - // append to tx_outputs - // - // The UtxoNotifyType (Onchain or Offchain) is automatically detected - // based on whether the address belongs to our wallet or not. - tx_outputs.push(TxOutput::auto( - &self.wallet_state, - &address, - amount, - sender_randomness, - )?); - } + let tx_outputs: Vec<_> = outputs + .into_iter() + .map(|(address, amount)| { + let sender_randomness = self + .wallet_state + .wallet_secret + .generate_sender_randomness(block_height, address.privacy_digest()); + + // The UtxoNotifyMethod (Onchain or Offchain) is auto-detected + // based on whether the address belongs to our wallet or not + TxOutput::auto( + &self.wallet_state, + &address, + amount, + sender_randomness, + owned_utxo_notify_method, + ) + }) + .collect::>()?; - Ok(tx_outputs) + Ok(tx_outputs.into()) } /// creates a Transaction. /// /// This API provides a simple-to-use interface for creating a transaction. - /// Utxo inputs are automatically chosen and a change output is + /// [Utxo] inputs are automatically chosen and a change output is /// automatically created, such that: /// /// change = sum(inputs) - sum(outputs) - fee. @@ -466,23 +477,24 @@ impl GlobalState { /// can be used instead. /// /// The `tx_outputs` parameter should normally be generated with - /// [Self::generate_tx_outputs]. This will generate OffChain - /// notifications for UTXOs destined for our wallet and OnChainPubKey - /// notifications for all other UTXOs. + /// [Self::generate_tx_outputs()] which determines which outputs should be + /// `OnChain` or `OffChain`. /// - /// It is the caller's responsibility to inform the wallet of any expected - /// utxos, ie offchain secret notifications, for utxos that match wallet - /// keys. + /// After this call returns it is the caller's responsibility to inform the + /// wallet of any returned [ExpectedUtxo], ie `OffChain` secret + /// notifications, for utxos that match wallet keys. Failure to do so can + /// result in loss of funds! /// /// This function will modify the `tx_outputs` parameter by /// appending an element representing the change output, if change is - /// needed. Expected utxos, including change can then be retrieved + /// needed. Any [ExpectedUtxo], including change can then be retrieved /// with [TxOutputList::expected_utxos()]. /// /// The `change_utxo_notify_method` parameter should normally be - /// UtxoNotifyMethod::OffChain in order to save blockchain space. - /// Note however there is a risk of losing funds with offchain - /// notification if local state is lost. + /// [UtxoNotifyMethod::OnChain] for safest transfer. + /// + /// The change_key should normally be a [SpendingKeyType::Symmetric] in + /// order to save blockchain space compared to a regular address. /// /// Note that `create_transaction()` does not modify any state and does not /// require acquiring write lock. This is important becauce internally it @@ -492,28 +504,36 @@ impl GlobalState { /// /// ```compile_fail /// - /// // we obtain a change_address first, as it requires modifying wallet state. - /// let change_spending_key = global_state_lock + /// // we obtain a change key first, as it requires modifying wallet state. + /// // note that this is a SymmetricKey, not a regular (Generation) address. + /// let change_key = global_state_lock /// .lock_guard_mut() /// .await /// .wallet_state /// .wallet_secret - /// .next_unused_generation_spending_key(); + /// .next_unused_spending_key(KeyType::Symmetric); + /// + /// // we choose onchain notification for all utxos destined for our wallet. + /// let notify_method = UtxoNotifyMethod::OnChain; /// /// // obtain read lock /// let state = self.state.lock_guard().await; - /// let mut tx_outputs = state.generate_tx_outputs(outputs)?; + /// + /// // generate the tx_outputs + /// let mut tx_outputs = state.generate_tx_outputs(outputs, notify_method)?; /// /// // Create the transaction /// let transaction = state /// .create_transaction( - /// &mut tx_outputs, - /// change_spending_key.into(), - /// UtxoNotifyMethod::OffChain, // notify change utxo offchain + /// &mut tx_outputs, // all outputs except `change` + /// change_key, // send `change` to this key + /// notify_method, // how to notify about `change` utxo /// NeptuneCoins::new(2), // fee /// Timestamp::now(), /// ) /// .await?; + /// + /// // drop read lock. /// drop(state); /// /// // Inform wallet of any expected incoming utxos. @@ -526,7 +546,7 @@ impl GlobalState { pub async fn create_transaction( &self, tx_outputs: &mut TxOutputList, - change_spending_key: SpendingKeyType, + change_key: SpendingKey, change_utxo_notify_method: UtxoNotifyMethod, fee: NeptuneCoins, timestamp: Timestamp, @@ -546,36 +566,34 @@ impl GlobalState { if total_spend < input_amount { let block_height = self.chain.light_state().header().height; - let change_address = change_spending_key.to_address(); let amount = input_amount.checked_sub(&total_spend).ok_or_else(|| { anyhow::anyhow!("underflow subtracting total_spend from input_amount") })?; - let utxo = Utxo::new_native_coin(change_address.lock_script(), amount); + let tx_output = { + let utxo = Utxo::new_native_coin(change_key.to_address().lock_script(), amount); + let sender_randomness = self.wallet_state.wallet_secret.generate_sender_randomness( + block_height, + change_key.to_address().privacy_digest(), + ); - let sender_randomness = self - .wallet_state - .wallet_secret - .generate_sender_randomness(block_height, change_address.privacy_digest()); - - let tx_output = match change_utxo_notify_method { - UtxoNotifyMethod::OnChainPubKey => { - let public_announcement = - change_address.generate_public_announcement(&utxo, sender_randomness)?; - TxOutput::onchain_pubkey( - utxo, - sender_randomness, - change_address.privacy_digest(), - public_announcement, - ) + match change_utxo_notify_method { + UtxoNotifyMethod::OnChain => { + let public_announcement = change_key + .to_address() + .generate_public_announcement(&utxo, sender_randomness)?; + TxOutput::onchain( + utxo, + sender_randomness, + change_key.to_address().privacy_digest(), + public_announcement, + ) + } + UtxoNotifyMethod::OffChain => { + TxOutput::offchain(utxo, sender_randomness, change_key.privacy_preimage()) + } } - UtxoNotifyMethod::OnChainSymmetricKey => unimplemented!(), - UtxoNotifyMethod::OffChain => TxOutput::offchain( - utxo, - sender_randomness, - change_spending_key.privacy_preimage(), - ), }; tx_outputs.push(tx_output); @@ -598,17 +616,17 @@ impl GlobalState { /// It is the caller's responsibility to provide inputs and outputs such /// that sum(inputs) == sum(outputs) + fee. Else an error will result. /// - /// Note that this means the caller must calculate the change amount if any + /// Note that this means the caller must calculate the `change` amount if any /// and provide an output for the change. /// /// The `tx_outputs` parameter should normally be generated with - /// [Self::generate_tx_outputs()]. This will generate OffChain - /// notifications for UTXOs destined for our wallet and OnChainPubKey - /// notifications for all other UTXOs. + /// [Self::generate_tx_outputs()] which determines which outputs should be + /// `OnChain` or `OffChain`. /// - /// It is the caller's responsibility to inform the wallet of any expected - /// utxos, ie offchain secret notifications, for utxos that match wallet - /// keys. + /// After this call returns it is the caller's responsibility to inform the + /// wallet of any returned [ExpectedUtxo], ie `OffChain` secret + /// notifications, for utxos that match wallet keys. Failure to do so can + /// result in loss of funds! /// /// Note that `create_raw_transaction()` does not modify any state and does /// not require acquiring write lock. This is important becauce internally @@ -645,16 +663,16 @@ impl GlobalState { // note: should use next_unused_generation_spending_key() // but that requires &mut self. - let change_spending_key = self + let change_key = self .wallet_state .wallet_secret - .nth_generation_spending_key(0); + .nth_symmetric_key_for_tests(0); let len = tx_outputs.len(); let transaction = self .create_transaction( &mut tx_outputs, - change_spending_key.into(), + change_key.into(), UtxoNotifyMethod::OffChain, fee, timestamp, @@ -1178,6 +1196,12 @@ impl GlobalState { Ok(removed_count) } + pub async fn persist_wallet(&mut self) -> Result<()> { + // flush wallet databases + self.wallet_state.wallet_db.persist().await; + Ok(()) + } + pub async fn flush_databases(&mut self) -> Result<()> { // flush wallet databases self.wallet_state.wallet_db.persist().await; @@ -1362,8 +1386,9 @@ mod global_state_tests { }, }; use num_traits::{One, Zero}; - use rand::{rngs::StdRng, thread_rng, Rng, SeedableRng}; + use rand::{random, rngs::StdRng, thread_rng, Rng, SeedableRng}; use tracing_test::traced_test; + use wallet::address::{generation_address::GenerationReceivingAddress, KeyType}; use super::{wallet::WalletSecret, *}; @@ -1403,18 +1428,21 @@ mod global_state_tests { let genesis_block = Block::genesis_block(network); let twenty_neptune: NeptuneCoins = NeptuneCoins::new(20); let twenty_coins = twenty_neptune.to_native_coins(); - let recipient_address = other_wallet.nth_generation_spending_key(0).to_address(); + let recipient_address: ReceivingAddress = other_wallet + .nth_generation_spending_key_for_tests(0) + .to_address() + .into(); let main_lock_script = recipient_address.lock_script(); let output_utxo = Utxo { coins: twenty_coins, lock_script_hash: main_lock_script.hash(), }; let sender_randomness = Digest::default(); - let receiver_privacy_digest = recipient_address.privacy_digest; + let receiver_privacy_digest = recipient_address.privacy_digest(); let public_announcement = recipient_address .generate_public_announcement(&output_utxo, sender_randomness) .unwrap(); - let tx_outputs = vec![TxOutput::onchain_pubkey( + let tx_outputs = vec![TxOutput::onchain( output_utxo.clone(), sender_randomness, receiver_privacy_digest, @@ -1483,19 +1511,22 @@ mod global_state_tests { for i in 2..5 { let amount: NeptuneCoins = NeptuneCoins::new(i); let that_many_coins = amount.to_native_coins(); - let receiving_address = other_wallet.nth_generation_spending_key(0).to_address(); + let receiving_address: ReceivingAddress = other_wallet + .nth_generation_spending_key_for_tests(0) + .to_address() + .into(); let lock_script = receiving_address.lock_script(); let utxo = Utxo { coins: that_many_coins, lock_script_hash: lock_script.hash(), }; let other_sender_randomness = Digest::default(); - let other_receiver_digest = receiving_address.privacy_digest; + let other_receiver_digest = receiving_address.privacy_digest(); let other_public_announcement = receiving_address .generate_public_announcement(&utxo, other_sender_randomness) .unwrap(); output_utxos.push(utxo.clone()); - other_tx_outputs.push(TxOutput::onchain_pubkey( + other_tx_outputs.push(TxOutput::onchain( utxo, other_sender_randomness, other_receiver_digest, @@ -1535,7 +1566,7 @@ mod global_state_tests { let global_state_lock = mock_genesis_global_state(network, 2, devnet_wallet).await; let mut global_state = global_state_lock.lock_guard_mut().await; let other_receiver_address = WalletSecret::new_random() - .nth_generation_spending_key(0) + .nth_generation_spending_key_for_tests(0) .to_address(); let genesis_block = Block::genesis_block(network); let (mock_block_1, _, _) = @@ -1614,7 +1645,7 @@ mod global_state_tests { let other_receiver_wallet_secret = WalletSecret::new_random(); let other_receiver_address = other_receiver_wallet_secret - .nth_generation_spending_key(0) + .nth_generation_spending_key_for_tests(0) .to_address(); // 1. Create new block 1 and store it to the DB @@ -1684,7 +1715,7 @@ mod global_state_tests { let own_spending_key = global_state .wallet_state .wallet_secret - .nth_generation_spending_key(0); + .nth_generation_spending_key_for_tests(0); let own_receiving_address = own_spending_key.to_address(); // 1. Create new block 1a where we receive a coinbase UTXO, store it @@ -1714,7 +1745,7 @@ mod global_state_tests { // Make a new fork from genesis that makes us lose the coinbase UTXO of block 1a let other_wallet_secret = WalletSecret::new_random(); let other_receiving_address = other_wallet_secret - .nth_generation_spending_key(0) + .nth_generation_spending_key_for_tests(0) .to_address(); let mut parent_block = genesis_block; for _ in 0..5 { @@ -1768,11 +1799,11 @@ mod global_state_tests { mock_genesis_global_state(network, 2, WalletSecret::devnet_wallet()).await; let mut global_state = global_state_lock.lock_guard_mut().await; let wallet_secret = global_state.wallet_state.wallet_secret.clone(); - let own_spending_key = wallet_secret.nth_generation_spending_key(0); + let own_spending_key = wallet_secret.nth_generation_spending_key_for_tests(0); let own_receiving_address = own_spending_key.to_address(); let other_wallet_secret = WalletSecret::new_random(); let other_receiving_address = other_wallet_secret - .nth_generation_spending_key(0) + .nth_generation_spending_key_for_tests(0) .to_address(); // 1. Create new block 1a where we receive a coinbase UTXO, store it @@ -1956,16 +1987,16 @@ mod global_state_tests { mock_genesis_wallet_state(WalletSecret::devnet_wallet(), network).await; let genesis_spending_key = genesis_wallet_state .wallet_secret - .nth_generation_spending_key(0); + .nth_generation_spending_key_for_tests(0); let genesis_state_lock = mock_genesis_global_state(network, 3, genesis_wallet_state.wallet_secret).await; let wallet_secret_alice = WalletSecret::new_pseudorandom(rng.gen()); - let alice_spending_key = wallet_secret_alice.nth_generation_spending_key(0); + let alice_spending_key = wallet_secret_alice.nth_generation_spending_key_for_tests(0); let alice_state_lock = mock_genesis_global_state(network, 3, wallet_secret_alice).await; let wallet_secret_bob = WalletSecret::new_pseudorandom(rng.gen()); - let bob_spending_key = wallet_secret_bob.nth_generation_spending_key(0); + let bob_spending_key = wallet_secret_bob.nth_generation_spending_key_for_tests(0); let bob_state_lock = mock_genesis_global_state(network, 3, wallet_secret_bob).await; let genesis_block = Block::genesis_block(network); @@ -1983,7 +2014,7 @@ mod global_state_tests { let fee = NeptuneCoins::one(); let sender_randomness: Digest = rng.gen(); let tx_outputs_for_alice = vec![ - TxOutput::fake_announcement( + TxOutput::fake_address( Utxo { lock_script_hash: alice_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(41).to_native_coins(), @@ -1991,7 +2022,7 @@ mod global_state_tests { sender_randomness, alice_spending_key.to_address().privacy_digest, ), - TxOutput::fake_announcement( + TxOutput::fake_address( Utxo { lock_script_hash: alice_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(59).to_native_coins(), @@ -2003,7 +2034,7 @@ mod global_state_tests { // Two outputs for Bob let tx_outputs_for_bob = vec![ - TxOutput::fake_announcement( + TxOutput::fake_address( Utxo { lock_script_hash: bob_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(141).to_native_coins(), @@ -2011,7 +2042,7 @@ mod global_state_tests { sender_randomness, bob_spending_key.to_address().privacy_digest, ), - TxOutput::fake_announcement( + TxOutput::fake_address( Utxo { lock_script_hash: bob_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(59).to_native_coins(), @@ -2142,7 +2173,7 @@ mod global_state_tests { // Make two transactions: Alice sends two UTXOs to Genesis and Bob sends three UTXOs to genesis let tx_outputs_from_alice = vec![ - TxOutput::fake_announcement( + TxOutput::fake_address( Utxo { lock_script_hash: genesis_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(50).to_native_coins(), @@ -2150,7 +2181,7 @@ mod global_state_tests { rng.gen(), genesis_spending_key.to_address().privacy_digest, ), - TxOutput::fake_announcement( + TxOutput::fake_address( Utxo { lock_script_hash: genesis_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(49).to_native_coins(), @@ -2180,7 +2211,7 @@ mod global_state_tests { .unwrap(); let tx_outputs_from_bob = vec![ - TxOutput::fake_announcement( + TxOutput::fake_address( Utxo { lock_script_hash: genesis_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(50).to_native_coins(), @@ -2188,7 +2219,7 @@ mod global_state_tests { rng.gen(), genesis_spending_key.to_address().privacy_digest, ), - TxOutput::fake_announcement( + TxOutput::fake_address( Utxo { lock_script_hash: genesis_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(50).to_native_coins(), @@ -2196,7 +2227,7 @@ mod global_state_tests { rng.gen(), genesis_spending_key.to_address().privacy_digest, ), - TxOutput::fake_announcement( + TxOutput::fake_address( Utxo { lock_script_hash: genesis_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(98).to_native_coins(), @@ -2253,7 +2284,9 @@ mod global_state_tests { let now = genesis_block.kernel.header.timestamp; let wallet_secret = WalletSecret::new_random(); - let receiving_address = wallet_secret.nth_generation_spending_key(0).to_address(); + let receiving_address = wallet_secret + .nth_generation_spending_key_for_tests(0) + .to_address(); let (block_1, _cb_utxo, _cb_output_randomness) = make_mock_block_with_valid_pow(&genesis_block, None, receiving_address, rng.gen()); @@ -2264,4 +2297,292 @@ mod global_state_tests { .light_state() .is_valid(&genesis_block, now)); } + + /// tests that pertain to restoring a wallet from seed-phrase + /// and comparing onchain vs offchain notification methods. + mod restore_wallet { + use super::*; + + /// test scenario: onchain/symmetric. + /// pass outcome: no funds loss + /// + /// test described in [change_exists()] + #[traced_test] + #[tokio::test] + #[allow(clippy::needless_return)] + async fn onchain_symmetric_change_exists() -> Result<()> { + change_exists(UtxoNotifyMethod::OnChain, KeyType::Symmetric).await + } + + /// test scenario: onchain/generation. + /// pass outcome: no funds loss + /// + /// test described in [change_exists()] + #[traced_test] + #[tokio::test] + #[allow(clippy::needless_return)] + async fn onchain_generation_change_exists() -> Result<()> { + change_exists(UtxoNotifyMethod::OnChain, KeyType::Generation).await + } + + /// test scenario: offchain/symmetric. + /// pass outcome: all funds lost! + /// + /// test described in [change_exists()] + #[traced_test] + #[tokio::test] + #[allow(clippy::needless_return)] + async fn offchain_symmetric_change_exists() -> Result<()> { + change_exists(UtxoNotifyMethod::OffChain, KeyType::Symmetric).await + } + + /// test scenario: offchain/generation. + /// pass outcome: all funds lost! + /// + /// test described in [change_exists()] + #[traced_test] + #[tokio::test] + #[allow(clippy::needless_return)] + async fn offchain_generation_change_exists() -> Result<()> { + change_exists(UtxoNotifyMethod::OffChain, KeyType::Generation).await + } + + /// basic scenario: alice receives 20,000 coins in the premine. 7 months + /// after launch she sends 20 coins to bob, plus 1 coin fee. alice should + /// receive change of 19979. Sometime after this block is mined alice's + /// hard drive crashes and she loses her wallet. She still has her wallet + /// seed and uses it to create a new wallet and scan blockchain to recover + /// funds. At the end alice checks her wallet balance, which should be + /// 19979. + /// + /// note: the pre-mine and 7-months aspects are unimportant. This test + /// would have same results if alice were a coinbase recipient instead. + /// + /// variations: + /// utxo_notify_method: alice can choose OnChain or OffChain utxo notification. + /// change_key_type: alice's change key can be Symmetric or Generation + /// + /// outcomes: + /// onchain/symmetric: balance: 19979. no funds loss. + /// onchain/generation: balance: 19979. no funds loss. + /// offchain/symmetric: balance: 0. all funds lost! + /// offchain/generation: balance: 0. all funds lost! + /// + /// this function expects the above possible outcomes. ie, it passes when + /// it encounters those outcomes. + /// + /// These outcomes highlight the danger of using off-chain notification. + /// Even though alice stored her seed safely offline she still loses all her + /// funds. + /// + /// It is important to recognize that alice's hard drive may crash (or + /// device stolen, etc) at any moment after she sends the transaction. If + /// it happens 10 minutes after the transaction its unlikely she would have + /// a wallet backup. Or it could happen years after the transaction, + /// demonstrating that alice's wallet needs to be backed up in perpetuity. + /// + /// From this, we conclude that the only way alice could really use offchain + /// notification safely is if her wallet is stored on some kind of redundant + /// storage media that is expected to exist in perpetuity. + /// + /// Since most people do not have home raid arrays and regular backup + /// schedules it seems that offchain notifications are best suited for + /// scenarios where the wallet is stored encrypted on some kind of cloud + /// storage, whether centralized or decentralized. + /// + /// It may also be a business opportunity for hardware vendors to sell + /// redundant-storage-in-a-box to users that want to use offchain + /// notification but keep their wallets local. + async fn change_exists( + utxo_notify_method: UtxoNotifyMethod, + change_key_type: KeyType, + ) -> Result<()> { + // setup initial conditions + let network = Network::RegTest; + let genesis_block = Block::genesis_block(network); + let launch = genesis_block.kernel.header.timestamp; + let seven_months_post_launch = launch + Timestamp::months(7); + let miner_address = GenerationReceivingAddress::derive_from_seed(random()); + + // amounts used in alice-to-bob transaction. + let alice_to_bob_amount = NeptuneCoins::new(20); + let alice_to_bob_fee = NeptuneCoins::new(1); + + // init global state for alice bob + let alice_state_lock = + mock_genesis_global_state(network, 3, WalletSecret::devnet_wallet()).await; + let bob_state_lock = + mock_genesis_global_state(network, 3, WalletSecret::new_random()).await; + + // in bob wallet: create receiving address for bob + let bob_address = { + bob_state_lock + .lock_guard_mut() + .await + .wallet_state + .next_unused_spending_key(KeyType::Generation) + .to_address() + }; + + // in alice wallet: send pre-mined funds to bob + let block_1 = { + let mut alice_state_mut = alice_state_lock.lock_guard_mut().await; + + // store and verify alice's initial balance from pre-mine. + let alice_initial_balance = alice_state_mut + .get_wallet_status_for_tip() + .await + .synced_unspent_available_amount(seven_months_post_launch); + assert_eq!(alice_initial_balance, 20000u32.into()); + + // create change key for alice. change_key_type is a test param. + let alice_change_key = alice_state_mut + .wallet_state + .next_unused_spending_key(change_key_type); + + // create an output for bob, worth 20. + let outputs = vec![(bob_address, alice_to_bob_amount)]; + let mut tx_outputs = + alice_state_mut.generate_tx_outputs(outputs, utxo_notify_method)?; + + // create tx. utxo_notify_method is a test param. + let alice_to_bob_tx = alice_state_mut + .create_transaction( + &mut tx_outputs, + alice_change_key, + utxo_notify_method, + alice_to_bob_fee, + seven_months_post_launch, + ) + .await?; + + // Inform alice wallet of any expected incoming utxos. + // note: no-op when all utxo notifications are sent on-chain. + alice_state_mut + .add_expected_utxos_to_wallet(tx_outputs.expected_utxos_iter()) + .await?; + + // the block gets mined. + let (mut block_1, ..) = + make_mock_block_with_valid_pow(&genesis_block, None, miner_address, random()); + + // add tx to block. (weird this can happen) + block_1 + .accumulate_transaction( + alice_to_bob_tx, + &alice_state_mut + .chain + .archival_state() + .genesis_block() + .kernel + .body + .mutator_set_accumulator, + ) + .await; + + // alice's node learns of the new block. + alice_state_mut.set_new_tip(block_1.clone()).await?; + + // alice should have 2 monitored utxos. + assert_eq!( + 2, + alice_state_mut + .wallet_state + .wallet_db + .monitored_utxos() + .len().await, "Alice must have 2 UTXOs after block 1: change from transaction, and the spent premine UTXO" + ); + + // Now alice should have a balance of 19979. + // 20000 from premine - 21 (20 to Bob + 1 fee) + let alice_calculated_balance = alice_initial_balance + .checked_sub(&alice_to_bob_amount) + .unwrap() + .checked_sub(&alice_to_bob_fee) + .unwrap(); + assert_eq!(alice_calculated_balance, 19979u32.into()); + + assert_eq!( + alice_calculated_balance, + alice_state_mut + .get_wallet_status_for_tip() + .await + .synced_unspent_available_amount(seven_months_post_launch) + ); + + block_1 + }; + + // in bob's wallet + { + let mut bob_state_mut = bob_state_lock.lock_guard_mut().await; + + // bob's node adds block1 to the chain. + bob_state_mut.set_new_tip(block_1.clone()).await?; + + // Now Bob should have a balance of 20, from Alice + assert_eq!( + alice_to_bob_amount, // 20 + bob_state_mut + .get_wallet_status_for_tip() + .await + .synced_unspent_available_amount(seven_months_post_launch) + ); + } + + // some time in the future. minutes, months, or years... + + // oh no! alice's hard-drive crashes and she loses her wallet. + drop(alice_state_lock); + + // Fortunately alice still has her seed that she can restore from. + { + // devnet_wallet() stands in for alice's seed. + let alice_restored_state_lock = + mock_genesis_global_state(network, 3, WalletSecret::devnet_wallet()).await; + + let mut alice_state_mut = alice_restored_state_lock.lock_guard_mut().await; + + // check alice's initial balance after genesis. + let alice_initial_balance = alice_state_mut + .get_wallet_status_for_tip() + .await + .synced_unspent_available_amount(seven_months_post_launch); + + // lucky alice's wallet begins with 20000 balance from premine. + assert_eq!(alice_initial_balance, 20000u32.into()); + + // now alice must replay old blocks. (there's only one so far) + alice_state_mut.set_new_tip(block_1).await?; + + // Now alice should have a balance of 19979. + // 20000 from premine - 21 (20 to Bob + 1 fee) + let alice_calculated_balance = alice_initial_balance + .checked_sub(&alice_to_bob_amount) + .unwrap() + .checked_sub(&alice_to_bob_fee) + .unwrap(); + + assert_eq!(alice_calculated_balance, 19979u32.into()); + + // For onchain notification the balance will be 19979. + // For offchain notification, it will be 0. Funds are lost!!! + let alice_expected_balance_by_method = match utxo_notify_method { + UtxoNotifyMethod::OnChain => NeptuneCoins::new(19979), + UtxoNotifyMethod::OffChain => NeptuneCoins::new(0), + }; + + // verify that our on/offchain prediction is correct. + assert_eq!( + alice_expected_balance_by_method, + alice_state_mut + .get_wallet_status_for_tip() + .await + .synced_unspent_available_amount(seven_months_post_launch) + ); + } + + Ok(()) + } + } } diff --git a/src/models/state/wallet/address.rs b/src/models/state/wallet/address.rs index 567c21cc..91dd0fbc 100644 --- a/src/models/state/wallet/address.rs +++ b/src/models/state/wallet/address.rs @@ -1,10 +1,16 @@ mod address_type; +mod common; + pub mod generation_address; +pub mod symmetric_key; /// ReceivingAddressType abstracts over any address type and should be used /// wherever possible. -pub use address_type::ReceivingAddressType; +pub use address_type::ReceivingAddress; /// SpendingKeyType abstracts over any spending key type and should be used /// wherever possible. -pub use address_type::SpendingKeyType; +pub use address_type::SpendingKey; + +/// KeyType simply enumerates the known key types. +pub use address_type::KeyType; diff --git a/src/models/state/wallet/address/address_type.rs b/src/models/state/wallet/address/address_type.rs index 1ad9f252..47e60238 100644 --- a/src/models/state/wallet/address/address_type.rs +++ b/src/models/state/wallet/address/address_type.rs @@ -1,49 +1,171 @@ -use super::generation_address; -use crate::{ - config_models::network::Network, - models::blockchain::transaction::{ - utxo::{LockScript, Utxo}, - PublicAnnouncement, - }, -}; -use anyhow::Result; +//! provides an abstraction over key and address types. + +use super::common; +use super::{generation_address, symmetric_key}; +use crate::config_models::network::Network; +use crate::models::blockchain::shared::Hash; +use crate::models::blockchain::transaction::utxo::LockScript; +use crate::models::blockchain::transaction::utxo::Utxo; +use crate::models::blockchain::transaction::AnnouncedUtxo; +use crate::models::blockchain::transaction::PublicAnnouncement; +use crate::models::blockchain::transaction::Transaction; +use crate::prelude::twenty_first; +use crate::util_types::mutator_set::commit; +use crate::BFieldElement; +use anyhow::{bail, Result}; use serde::{Deserialize, Serialize}; use tasm_lib::triton_vm::prelude::Digest; +use tracing::warn; +use twenty_first::util_types::algebraic_hasher::AlgebraicHasher; + +// note: assigning the flags to `KeyType` variants as discriminants has bonus +// that we get a compiler verification that values do not conflict. which is +// nice since they are (presently) defined in separate files. +// +// anyway it is a desirable property that KeyType variants match the values +// actually stored in PublicAnnouncement. + +/// enumerates available cryptographic key implementations for sending and receiving funds. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[repr(u8)] +pub enum KeyType { + /// [generation_address] built on [twenty_first::math::lattice::kem] + /// + /// wraps a symmetric key built on aes-256-gcm + Generation = generation_address::GENERATION_FLAG_U8, + + /// [symmetric_key] built on aes-256-gcm + Symmetric = symmetric_key::SYMMETRIC_KEY_FLAG_U8, +} + +impl From<&ReceivingAddress> for KeyType { + fn from(addr: &ReceivingAddress) -> Self { + match addr { + ReceivingAddress::Generation(_) => Self::Generation, + ReceivingAddress::Symmetric(_) => Self::Symmetric, + } + } +} + +impl From<&SpendingKey> for KeyType { + fn from(addr: &SpendingKey) -> Self { + match addr { + SpendingKey::Generation(_) => Self::Generation, + SpendingKey::Symmetric(_) => Self::Symmetric, + } + } +} + +impl From for BFieldElement { + fn from(key_type: KeyType) -> Self { + (key_type as u8).into() + } +} + +impl TryFrom<&PublicAnnouncement> for KeyType { + type Error = anyhow::Error; + + fn try_from(pa: &PublicAnnouncement) -> Result { + match common::key_type_from_public_announcement(pa) { + Ok(kt) if kt == Self::Generation.into() => Ok(Self::Generation), + Ok(kt) if kt == Self::Symmetric.into() => Ok(Self::Symmetric), + _ => bail!("encountered PublicAnnouncement of unknown type"), + } + } +} + +impl KeyType { + /// returns all available `KeyType` + pub fn all_types() -> Vec { + vec![Self::Generation, Self::Symmetric] + } +} /// Represents any type of Neptune receiving Address. /// /// This enum provides an abstraction API for Address types, so that /// a method or struct may simply accept a `ReceivingAddressType` and be /// forward-compatible with new types of Address as they are implemented. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -pub enum ReceivingAddressType { - Generation(generation_address::ReceivingAddress), +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum ReceivingAddress { + /// a [generation_address] + Generation(Box), + + /// a [symmetric_key] acting as an address. + Symmetric(symmetric_key::SymmetricKey), +} + +impl From for ReceivingAddress { + fn from(a: generation_address::GenerationReceivingAddress) -> Self { + Self::Generation(Box::new(a)) + } +} + +impl From<&generation_address::GenerationReceivingAddress> for ReceivingAddress { + fn from(a: &generation_address::GenerationReceivingAddress) -> Self { + Self::Generation(Box::new(*a)) + } +} + +impl From for ReceivingAddress { + fn from(k: symmetric_key::SymmetricKey) -> Self { + Self::Symmetric(k) + } +} + +impl From<&symmetric_key::SymmetricKey> for ReceivingAddress { + fn from(k: &symmetric_key::SymmetricKey) -> Self { + Self::Symmetric(*k) + } } -impl From for ReceivingAddressType { - fn from(a: generation_address::ReceivingAddress) -> Self { - Self::Generation(a) +impl TryFrom for generation_address::GenerationReceivingAddress { + type Error = anyhow::Error; + + fn try_from(a: ReceivingAddress) -> Result { + match a { + ReceivingAddress::Generation(a) => Ok(*a), + _ => bail!("not a generation address"), + } } } -impl ReceivingAddressType { +impl ReceivingAddress { + /// returns `receiver_identifer` + pub fn receiver_identifier(&self) -> BFieldElement { + match self { + Self::Generation(a) => a.receiver_identifier, + Self::Symmetric(a) => a.receiver_identifier(), + } + } + /// generates a [PublicAnnouncement] for an output Utxo + /// + /// The public announcement contains a Vec type flag. (SYMMETRIC_KEY_FLAG) + /// 1 --> receiver_identifier (fingerprint derived from seed) + /// 2..n --> ciphertext (encrypted utxo + sender_randomness) + /// + /// Fields |0,1| enable the receiver to determine the ciphertext + /// is intended for them and decryption should be attempted. pub fn generate_public_announcement( &self, utxo: &Utxo, sender_randomness: Digest, ) -> Result { - match self { - Self::Generation(a) => a.generate_public_announcement(utxo, sender_randomness), - } + let ciphertext = [ + &[KeyType::from(self).into(), self.receiver_identifier()], + self.encrypt(utxo, sender_randomness)?.as_slice(), + ] + .concat(); + Ok(PublicAnnouncement::new(ciphertext)) } - /// Generate a [LockScript] from the spending lock. Satisfaction - /// of this lock script establishes the UTXO owner's assent to - /// the transaction. - pub fn lock_script(&self) -> LockScript { + /// returns the `spending_lock` + pub fn spending_lock(&self) -> Digest { match self { - Self::Generation(a) => a.lock_script(), + Self::Generation(a) => a.spending_lock, + Self::Symmetric(k) => k.spending_lock(), } } @@ -52,23 +174,59 @@ impl ReceivingAddressType { pub fn privacy_digest(&self) -> Digest { match self { Self::Generation(a) => a.privacy_digest, + Self::Symmetric(k) => k.privacy_digest(), + } + } + + /// encrypts a [Utxo] and `sender_randomness` secret for purpose of transferring to payment recipient + pub fn encrypt(&self, utxo: &Utxo, sender_randomness: Digest) -> Result> { + match self { + Self::Generation(a) => a.encrypt(utxo, sender_randomness), + Self::Symmetric(a) => Ok(a.encrypt(utxo, sender_randomness)?), } } /// encodes this address as bech32m + /// + /// note: this will return an error for symmetric keys as they do not impl + /// bech32m at present. There is no need to give them out to 3rd + /// parties in a serialized form. pub fn to_bech32m(&self, network: Network) -> Result { match self { Self::Generation(k) => k.to_bech32m(network), + Self::Symmetric(_k) => bail!("bech32m not implemented for symmetric keys"), } } /// parses an address from its bech32m encoding + /// + /// note: this will fail for Symmetric keys which do not impl bech32m + /// at present. There is no need to give them out to 3rd parties + /// in a serialized form. pub fn from_bech32m(encoded: &str, network: Network) -> Result { - let addr = generation_address::ReceivingAddress::from_bech32m(encoded, network)?; - Ok(Self::Generation(addr)) + let addr = generation_address::GenerationReceivingAddress::from_bech32m(encoded, network)?; + Ok(addr.into()) // when future addr types are supported, we would attempt each type in // turn. + + // note: not implemented for SymmetricKey (yet?) + } + + /// generates a lock script from the spending lock. + /// + /// Satisfaction of this lock script establishes the UTXO owner's assent to + /// the transaction. + pub fn lock_script(&self) -> LockScript { + match self { + Self::Generation(k) => k.lock_script(), + Self::Symmetric(k) => k.lock_script(), + } + } + + /// returns true if the [PublicAnnouncement] has a type-flag that matches the type of this address. + pub fn matches_public_announcement_key_type(&self, pa: &PublicAnnouncement) -> bool { + matches!(KeyType::try_from(pa), Ok(kt) if kt == KeyType::from(self)) } } @@ -78,21 +236,32 @@ impl ReceivingAddressType { /// method or struct may simply accept a `SpendingKeyType` and be /// forward-compatible with new types of spending key as they are implemented. #[derive(Debug, Clone, Copy)] -pub enum SpendingKeyType { - Generation(generation_address::SpendingKey), +pub enum SpendingKey { + /// a key from [generation_address] + Generation(generation_address::GenerationSpendingKey), + + /// a [symmetric_key] + Symmetric(symmetric_key::SymmetricKey), } -impl From for SpendingKeyType { - fn from(key: generation_address::SpendingKey) -> Self { +impl From for SpendingKey { + fn from(key: generation_address::GenerationSpendingKey) -> Self { Self::Generation(key) } } -impl SpendingKeyType { +impl From for SpendingKey { + fn from(key: symmetric_key::SymmetricKey) -> Self { + Self::Symmetric(key) + } +} + +impl SpendingKey { /// returns the address that corresponds to this spending key. - pub fn to_address(&self) -> ReceivingAddressType { + pub fn to_address(&self) -> ReceivingAddress { match self { Self::Generation(k) => k.to_address().into(), + Self::Symmetric(k) => (*k).into(), } } @@ -103,15 +272,294 @@ impl SpendingKeyType { pub fn privacy_preimage(&self) -> Digest { match self { Self::Generation(k) => k.privacy_preimage, + Self::Symmetric(k) => k.privacy_preimage(), + } + } + + /// returns the receiver_identifier, a public fingerprint + pub fn receiver_identifier(&self) -> BFieldElement { + match self { + Self::Generation(k) => k.receiver_identifier, + Self::Symmetric(k) => k.receiver_identifier(), } } /// returns unlock_key needed for transaction witnesses - /// - /// doc todo: better description pub fn unlock_key(&self) -> Digest { match self { Self::Generation(k) => k.unlock_key, + Self::Symmetric(k) => k.unlock_key(), + } + } + + /// decrypts an array of BFieldElement into a [Utxo] and [Digest] representing `sender_randomness`. + pub fn decrypt(&self, ciphertext_bfes: &[BFieldElement]) -> Result<(Utxo, Digest)> { + match self { + Self::Generation(k) => k.decrypt(ciphertext_bfes), + Self::Symmetric(k) => Ok(k.decrypt(ciphertext_bfes)?), + } + } + + /// scans public announcements in a [Transaction] and finds any that match this key + /// + /// note that a single [Transaction] may represent an entire block + /// + /// returns an iterator over [AnnouncedUtxo] + /// + /// side-effect: logs a warning for any announcement targeted at this key + /// that cannot be decypted. + pub fn scan_for_announced_utxos<'a>( + &'a self, + transaction: &'a Transaction, + ) -> impl Iterator + 'a { + // pre-compute some fields. + let receiver_identifier = self.receiver_identifier(); + let receiver_preimage = self.privacy_preimage(); + let receiver_digest = receiver_preimage.hash::(); + + // for all public announcements + transaction + .kernel + .public_announcements + .iter() + + // ... that are marked as encrypted to our key type + .filter(|pa| self.matches_public_announcement_key_type(pa)) + + // ... that match the receiver_id of this key + .filter(move |pa| { + matches!(common::receiver_identifier_from_public_announcement(pa), Ok(r) if r == receiver_identifier) + }) + + // ... that have a ciphertext field + .filter_map(|pa| self.ok_warn(common::ciphertext_from_public_announcement(pa)) ) + + // ... which can be decrypted with this key + .filter_map(|c| self.ok_warn(self.decrypt(&c))) + + // ... map to AnnouncedUtxo + .map(move |(utxo, sender_randomness)| { + // and join those with the receiver digest to get a commitment + // Note: the commitment is computed in the same way as in the mutator set. + AnnouncedUtxo { + addition_record: commit(Hash::hash(&utxo), sender_randomness, receiver_digest), + utxo, + sender_randomness, + receiver_preimage, + } + }) + } + + /// converts a result into an Option and logs a warning on any error + fn ok_warn(&self, result: Result) -> Option { + match result { + Ok(v) => Some(v), + Err(e) => { + warn!("possible loss of funds! skipping public announcement for symmetric key with receiver_identifier: {}. error: {}", self.receiver_identifier(), e.to_string()); + None + } + } + } + + /// returns true if the [PublicAnnouncement] has a type-flag that matches the type of this key + fn matches_public_announcement_key_type(&self, pa: &PublicAnnouncement) -> bool { + matches!(KeyType::try_from(pa), Ok(kt) if kt == KeyType::from(self)) + } +} + +#[cfg(test)] +mod test { + use super::*; + + use crate::models::blockchain::type_scripts::neptune_coins::NeptuneCoins; + use crate::tests::shared::make_mock_transaction; + use generation_address::GenerationReceivingAddress; + use generation_address::GenerationSpendingKey; + use itertools::Itertools; + use proptest_arbitrary_interop::arb; + use rand::random; + use rand::thread_rng; + use rand::Rng; + use symmetric_key::SymmetricKey; + use test_strategy::proptest; + + /// tests scanning for announced utxos with a symmetric key + #[proptest] + fn scan_for_announced_utxos_symmetric(#[strategy(arb())] seed: Digest) { + worker::scan_for_announced_utxos(SymmetricKey::from_seed(seed).into()) + } + + /// tests scanning for announced utxos with an asymmetric (generation) key + #[proptest] + fn scan_for_announced_utxos_generation(#[strategy(arb())] seed: Digest) { + worker::scan_for_announced_utxos(GenerationSpendingKey::derive_from_seed(seed).into()) + } + + /// tests encrypting and decrypting with a symmetric key + #[proptest] + fn test_encrypt_decrypt_symmetric(#[strategy(arb())] seed: Digest) { + worker::test_encrypt_decrypt(SymmetricKey::from_seed(seed).into()) + } + + /// tests encrypting and decrypting with an asymmetric (generation) key + #[proptest] + fn test_encrypt_decrypt_generation(#[strategy(arb())] seed: Digest) { + worker::test_encrypt_decrypt(GenerationSpendingKey::derive_from_seed(seed).into()) + } + + /// tests keygen, sign, and verify with a symmetric key + #[proptest] + fn test_keygen_sign_verify_symmetric(#[strategy(arb())] seed: Digest) { + worker::test_keygen_sign_verify( + SymmetricKey::from_seed(seed).into(), + SymmetricKey::from_seed(seed).into(), + ); + } + + /// tests keygen, sign, and verify with an asymmetric (generation) key + #[proptest] + fn test_keygen_sign_verify_generation(#[strategy(arb())] seed: Digest) { + worker::test_keygen_sign_verify( + GenerationSpendingKey::derive_from_seed(seed).into(), + GenerationReceivingAddress::derive_from_seed(seed).into(), + ); + } + + /// tests bech32m serialize, deserialize with a symmetric key + #[should_panic(expected = "bech32m not implemented for symmetric keys")] + #[proptest] + fn test_bech32m_conversion_symmetric(#[strategy(arb())] seed: Digest) { + worker::test_bech32m_conversion(SymmetricKey::from_seed(seed).into()); + } + + /// tests bech32m serialize, deserialize with an asymmetric (generation) key + #[proptest] + fn test_bech32m_conversion_generation(#[strategy(arb())] seed: Digest) { + worker::test_bech32m_conversion(GenerationReceivingAddress::derive_from_seed(seed).into()); + } + + mod worker { + use super::*; + + /// this tests the generate_public_announcement() and + /// scan_for_announced_utxos() methods with a [SpendingKeyType] + /// + /// a PublicAnnouncement is created with generate_public_announcement() and + /// added to a Tx. It is then found by scanning for announced_utoxs. Then + /// we verify that the data matches the original/expected values. + pub fn scan_for_announced_utxos(key: SpendingKey) { + // 1. generate a utxo with amount = 10 + let utxo = Utxo::new_native_coin(key.to_address().lock_script(), NeptuneCoins::new(10)); + + // 2. generate sender randomness + let sender_randomness: Digest = random(); + + // 3. create an addition record to verify against later. + let expected_addition_record = commit( + Hash::hash(&utxo), + sender_randomness, + key.to_address().privacy_digest(), + ); + + // 4. create a mock tx with no inputs or outputs + let mut mock_tx = make_mock_transaction(vec![], vec![]); + + // 5. verify that no announced utxos exist for this key + assert!(key + .scan_for_announced_utxos(&mock_tx) + .collect_vec() + .is_empty()); + + // 6. generate a public announcement for this address + let public_announcement = key + .to_address() + .generate_public_announcement(&utxo, sender_randomness) + .unwrap(); + + // 7. verify that the public_announcement is marked as our key type. + assert!(key.matches_public_announcement_key_type(&public_announcement)); + + // 8. add the public announcement to the mock tx. + mock_tx + .kernel + .public_announcements + .push(public_announcement); + + // 9. scan tx public announcements for announced utxos + let announced_utxos = key.scan_for_announced_utxos(&mock_tx).collect_vec(); + + // 10. verify there is exactly 1 announced_utxo and obtain it. + assert_eq!(1, announced_utxos.len()); + let announced_utxo = announced_utxos.into_iter().next().unwrap(); + + // 11. verify each field of the announced_utxo matches original values. + assert_eq!(utxo, announced_utxo.utxo); + assert_eq!(expected_addition_record, announced_utxo.addition_record); + assert_eq!(sender_randomness, announced_utxo.sender_randomness); + assert_eq!(key.privacy_preimage(), announced_utxo.receiver_preimage); + } + + /// This tests encrypting and decrypting with a [SpendingKeyType] + pub fn test_encrypt_decrypt(key: SpendingKey) { + let mut rng = thread_rng(); + + // 1. create utxo with random amount + let amount = NeptuneCoins::new(rng.gen_range(0..42000000)); + let utxo = Utxo::new_native_coin(key.to_address().lock_script(), amount); + + // 2. generate sender randomness + let sender_randomness: Digest = random(); + + // 3. encrypt secrets (utxo, sender_randomness) + let ciphertext = key.to_address().encrypt(&utxo, sender_randomness).unwrap(); + println!("ciphertext.get_size() = {}", ciphertext.len() * 8); + + // 4. decrypt secrets + let (utxo_again, sender_randomness_again) = key.decrypt(&ciphertext).unwrap(); + + // 5. verify that decrypted secrets match original secrets + assert_eq!(utxo, utxo_again); + assert_eq!(sender_randomness, sender_randomness_again); + } + + /// tests key generation, signing, and decrypting with a [SpendingKeyType] + /// + /// note: key generation is performed by the caller. Both the + /// spending_key and receiving_address must be independently derived from + /// the same seed. + pub fn test_keygen_sign_verify( + spending_key: SpendingKey, + receiving_address: ReceivingAddress, + ) { + // 1. prepare a (random) message and witness data. + let msg: Digest = random(); + let witness_data = common::test::binding_unlock(spending_key.unlock_key(), msg); + + // 2. perform mock proof verification + assert!(common::test::std_lockscript_reference_verify_unlock( + receiving_address.spending_lock(), + msg, + witness_data + )); + + // 3. convert spending key to an address. + let receiving_address_again = spending_key.to_address(); + + // 4. verify that both address match. + assert_eq!(receiving_address, receiving_address_again); + } + + /// tests bech32m serialize, deserialize for [ReceivingAddressType] + pub fn test_bech32m_conversion(receiving_address: ReceivingAddress) { + // 1. serialize address to bech32m + let encoded = receiving_address.to_bech32m(Network::Testnet).unwrap(); + + // 2. deserialize bech32m back into an address + let receiving_address_again = + ReceivingAddress::from_bech32m(&encoded, Network::Testnet).unwrap(); + + // 3. verify both addresses match + assert_eq!(receiving_address, receiving_address_again); } } } diff --git a/src/models/state/wallet/address/common.rs b/src/models/state/wallet/address/common.rs new file mode 100644 index 00000000..3ed65d77 --- /dev/null +++ b/src/models/state/wallet/address/common.rs @@ -0,0 +1,226 @@ +use crate::models::blockchain::shared::Hash; +use crate::models::blockchain::transaction::utxo::LockScript; +use crate::models::blockchain::transaction::PublicAnnouncement; +use crate::prelude::triton_vm; +use crate::prelude::twenty_first; +use anyhow::bail; +use anyhow::Result; +use itertools::Itertools; +use sha3::digest::ExtendableOutput; +use sha3::digest::Update; +use sha3::Shake256; +use triton_vm::triton_asm; +use triton_vm::triton_instr; +use twenty_first::math::b_field_element::BFieldElement; +use twenty_first::math::tip5::Digest; +use twenty_first::util_types::algebraic_hasher::AlgebraicHasher; + +/// Derive a receiver id from a seed. +pub fn derive_receiver_id(seed: Digest) -> BFieldElement { + Hash::hash_varlen(&[seed.values().to_vec(), vec![BFieldElement::new(2)]].concat()).values()[0] +} + +/// retrieves key-type field from a [PublicAnnouncement] +/// +/// returns an error if the field is not present +pub fn key_type_from_public_announcement( + announcement: &PublicAnnouncement, +) -> Result { + match announcement.message.first() { + Some(key_type) => Ok(*key_type), + None => bail!("Public announcement does not contain key type."), + } +} + +/// retrieves ciphertext field from a [PublicAnnouncement] +/// +/// returns an error if the input is too short +pub fn ciphertext_from_public_announcement( + announcement: &PublicAnnouncement, +) -> Result> { + if announcement.message.len() <= 2 { + bail!("Public announcement does not contain ciphertext."); + } + Ok(announcement.message[2..].to_vec()) +} + +/// retrieves receiver identifier field from a [PublicAnnouncement] +/// +/// returns an error if the input is too short +pub fn receiver_identifier_from_public_announcement( + announcement: &PublicAnnouncement, +) -> Result { + match announcement.message.get(1) { + Some(id) => Ok(*id), + None => bail!("Public announcement does not contain receiver ID"), + } +} + +/// Encodes a slice of bytes to a vec of BFieldElements. This +/// encoding is injective but not uniform-to-uniform. +pub fn bytes_to_bfes(bytes: &[u8]) -> Vec { + let mut padded_bytes = bytes.to_vec(); + while padded_bytes.len() % 8 != 0 { + padded_bytes.push(0u8); + } + let mut bfes = vec![BFieldElement::new(bytes.len() as u64)]; + for chunk in padded_bytes.chunks(8) { + let ch: [u8; 8] = chunk.try_into().unwrap(); + let int = u64::from_be_bytes(ch); + if int < BFieldElement::P - 1 { + bfes.push(BFieldElement::new(int)); + } else { + let rem = int & 0xffffffff; + bfes.push(BFieldElement::new(BFieldElement::P - 1)); + bfes.push(BFieldElement::new(rem)); + } + } + bfes +} + +/// Decodes a slice of BFieldElements to a vec of bytes. This method +/// computes the inverse of `bytes_to_bfes`. +pub fn bfes_to_bytes(bfes: &[BFieldElement]) -> Result> { + if bfes.is_empty() { + bail!("Cannot decode empty byte stream"); + } + + let length = bfes[0].value() as usize; + if length > std::mem::size_of_val(bfes) { + bail!("Cannot decode byte stream shorter than length indicated. BFE slice length: {}, indicated byte stream length: {length}", bfes.len()); + } + + let mut bytes: Vec = Vec::with_capacity(length); + let mut skip_top = false; + for bfe in bfes.iter().skip(1) { + let bfe_bytes = bfe.value().to_be_bytes(); + if skip_top { + bytes.append(&mut bfe_bytes[4..8].to_vec()); + skip_top = false; + } else { + bytes.append(&mut bfe_bytes[0..4].to_vec()); + if bfe_bytes[0..4] == [0xff, 0xff, 0xff, 0xff] { + skip_top = true; + } else { + bytes.append(&mut bfe_bytes[4..8].to_vec()); + } + } + } + + Ok(bytes[0..length].to_vec()) +} + +// note: copied from twenty_first::math::lattice::kem::shake256() +// which is not public +pub fn shake256(randomness: impl AsRef<[u8]>) -> [u8; NUM_OUT_BYTES] { + let mut hasher = Shake256::default(); + hasher.update(randomness.as_ref()); + + let mut result = [0u8; NUM_OUT_BYTES]; + hasher.finalize_xof_into(&mut result); + result +} + +/// generates a lock script from the spending lock. +/// +/// Satisfaction of this lock script establishes the UTXO owner's assent to +/// the transaction. +pub fn lock_script(spending_lock: Digest) -> LockScript { + let push_spending_lock_digest_to_stack = spending_lock + .values() + .iter() + .rev() + .map(|elem| triton_instr!(push elem.value())) + .collect_vec(); + + let instructions = triton_asm!( + divine 5 + hash + {&push_spending_lock_digest_to_stack} + assert_vector + read_io 5 + halt + ); + + instructions.into() +} + +#[cfg(test)] +pub(super) mod test { + use super::*; + use rand::{thread_rng, Rng, RngCore}; + use tasm_lib::DIGEST_LENGTH; + + #[test] + fn test_conversion_fixed_length() { + let mut rng = thread_rng(); + const N: usize = 23; + let byte_array: [u8; N] = rng.gen(); + let byte_vec = byte_array.to_vec(); + let bfes = bytes_to_bfes(&byte_vec); + let bytes_again = bfes_to_bytes(&bfes).unwrap(); + + assert_eq!(byte_vec, bytes_again); + } + + #[test] + fn test_conversion_variable_length() { + let mut rng = thread_rng(); + for _ in 0..1000 { + let n: usize = rng.gen_range(0..101); + let mut byte_vec: Vec = vec![0; n]; + rng.try_fill_bytes(&mut byte_vec).unwrap(); + let bfes = bytes_to_bfes(&byte_vec); + let bytes_again = bfes_to_bytes(&bfes).unwrap(); + + assert_eq!(byte_vec, bytes_again); + } + } + + #[test] + fn test_conversion_cornercases() { + for test_case in [ + vec![], + vec![0u8], + vec![0u8, 0u8], + vec![0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8], + vec![0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8], + vec![0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8], + vec![1u8], + 0xffffffff00000000u64.to_be_bytes().to_vec(), + 0xffffffff00000001u64.to_be_bytes().to_vec(), + 0xffffffff00000123u64.to_be_bytes().to_vec(), + 0xffffffffffffffffu64.to_be_bytes().to_vec(), + [ + 0xffffffffffffffffu64.to_be_bytes().to_vec(), + 0xffffffffffffffffu64.to_be_bytes().to_vec(), + ] + .concat(), + ] { + let bfes = bytes_to_bfes(&test_case); + let bytes_again = bfes_to_bytes(&bfes).unwrap(); + + assert_eq!(test_case, bytes_again); + } + } + + /// Verify the UTXO owner's assent to the transaction. + /// This is the rust reference implementation, but the version of + /// this logic that is proven is `lock_script`. + /// + /// This function mocks proof verification. + pub fn std_lockscript_reference_verify_unlock( + spending_lock: Digest, + _bind_to: Digest, + witness_data: [BFieldElement; DIGEST_LENGTH], + ) -> bool { + spending_lock == Digest::new(witness_data).hash::() + } + + /// Unlock the UTXO binding it to some transaction by its kernel hash. + /// This function mocks proof generation. + pub fn binding_unlock(unlock_key: Digest, _bind_to: Digest) -> [BFieldElement; DIGEST_LENGTH] { + let witness_data = unlock_key; + witness_data.values() + } +} diff --git a/src/models/state/wallet/address/generation_address.rs b/src/models/state/wallet/address/generation_address.rs index 6a5b57b4..2a6979ca 100644 --- a/src/models/state/wallet/address/generation_address.rs +++ b/src/models/state/wallet/address/generation_address.rs @@ -1,6 +1,23 @@ -use crate::prelude::{triton_vm, twenty_first}; -use crate::util_types::mutator_set::commit; - +//! provides an asymmetric key interface for sending and claiming [Utxo]. +//! +//! The asymmetric key is based on [lattice::kem] and encrypts a symmetric key +//! based on [aes_gcm::Aes256Gcm] which encrypts the actual payload. +//! +//! ### Naming +//! +//! These are called "Generation" keys because they are quantum-secure and it is +//! believed/hoped that the cryptography should be unbreakable for at least a +//! generation and hopefully many generations. If correct, it would be safe to +//! put funds in a paper or metal wallet and ignore them for decades, perhaps +//! until they are transferred to the original owner's children or +//! grand-children. + +use super::common; +use crate::config_models::network::Network; +use crate::models::blockchain::shared::Hash; +use crate::models::blockchain::transaction::utxo::LockScript; +use crate::models::blockchain::transaction::utxo::Utxo; +use crate::prelude::twenty_first; use aead::Aead; use aead::KeyInit; use aes_gcm::Aes256Gcm; @@ -14,30 +31,17 @@ use rand::thread_rng; use rand::Rng; use serde_derive::Deserialize; use serde_derive::Serialize; -use sha3::digest::ExtendableOutput; -use sha3::digest::Update; -use sha3::Shake256; -use triton_vm::triton_asm; -use triton_vm::triton_instr; +use twenty_first::math::b_field_element::BFieldElement; +use twenty_first::math::lattice; use twenty_first::math::lattice::kem::CIPHERTEXT_SIZE_IN_BFES; -use twenty_first::math::tip5::DIGEST_LENGTH; -use twenty_first::{ - math::{b_field_element::BFieldElement, lattice, tip5::Digest}, - util_types::algebraic_hasher::AlgebraicHasher, -}; +use twenty_first::math::tip5::Digest; +use twenty_first::util_types::algebraic_hasher::AlgebraicHasher; -use crate::config_models::network::Network; -use crate::models::blockchain::shared::Hash; -use crate::models::blockchain::transaction::utxo::LockScript; -use crate::models::blockchain::transaction::utxo::Utxo; -use crate::models::blockchain::transaction::PublicAnnouncement; -use crate::models::blockchain::transaction::Transaction; -use crate::util_types::mutator_set::addition_record::AdditionRecord; - -pub const GENERATION_FLAG: BFieldElement = BFieldElement::new(79); +pub(super) const GENERATION_FLAG_U8: u8 = 79; +pub const GENERATION_FLAG: BFieldElement = BFieldElement::new(GENERATION_FLAG_U8 as u64); #[derive(Clone, Debug, Copy)] -pub struct SpendingKey { +pub struct GenerationSpendingKey { pub receiver_identifier: BFieldElement, pub decryption_key: lattice::kem::SecretKey, pub privacy_preimage: Digest, @@ -46,110 +50,19 @@ pub struct SpendingKey { } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -pub struct ReceivingAddress { +pub struct GenerationReceivingAddress { pub receiver_identifier: BFieldElement, pub encryption_key: lattice::kem::PublicKey, pub privacy_digest: Digest, pub spending_lock: Digest, } -/// Determine if the public announcement is flagged to indicate it might be a generation -/// address ciphertext. -fn public_announcement_is_marked(announcement: &PublicAnnouncement) -> bool { - matches!(announcement.message.first(), Some(&GENERATION_FLAG)) -} - -fn derive_receiver_id(seed: Digest) -> BFieldElement { - Hash::hash_varlen(&[seed.values().to_vec(), vec![BFieldElement::new(2)]].concat()).values()[0] -} - -fn receiver_identifier_from_public_announcement( - announcement: &PublicAnnouncement, -) -> Result { - match announcement.message.get(1) { - Some(id) => Ok(*id), - None => bail!("Public announcement does not contain receiver ID"), - } -} - -fn ciphertext_from_public_announcement( - announcement: &PublicAnnouncement, -) -> Result> { - if announcement.message.len() <= 2 { - bail!("Public announcement does not contain ciphertext."); - } - Ok(announcement.message[2..].to_vec()) -} - -/// Encodes a slice of bytes to a vec of BFieldElements. This -/// encoding is injective but not uniform-to-uniform. -pub fn bytes_to_bfes(bytes: &[u8]) -> Vec { - let mut padded_bytes = bytes.to_vec(); - while padded_bytes.len() % 8 != 0 { - padded_bytes.push(0u8); - } - let mut bfes = vec![BFieldElement::new(bytes.len() as u64)]; - for chunk in padded_bytes.chunks(8) { - let ch: [u8; 8] = chunk.try_into().unwrap(); - let int = u64::from_be_bytes(ch); - if int < BFieldElement::P - 1 { - bfes.push(BFieldElement::new(int)); - } else { - let rem = int & 0xffffffff; - bfes.push(BFieldElement::new(BFieldElement::P - 1)); - bfes.push(BFieldElement::new(rem)); - } - } - bfes -} - -/// Decodes a slice of BFieldElements to a vec of bytes. This method -/// computes the inverse of `bytes_to_bfes`. -pub fn bfes_to_bytes(bfes: &[BFieldElement]) -> Result> { - let length = bfes[0].value() as usize; - if length > std::mem::size_of_val(bfes) { - bail!("Cannot decode byte stream shorter than length indicated. BFE slice length: {}, indicated byte stream length: {length}", bfes.len()); - } - - let mut bytes: Vec = Vec::with_capacity(length); - let mut skip_top = false; - for bfe in bfes.iter().skip(1) { - let bfe_bytes = bfe.value().to_be_bytes(); - if skip_top { - bytes.append(&mut bfe_bytes[4..8].to_vec()); - skip_top = false; - } else { - bytes.append(&mut bfe_bytes[0..4].to_vec()); - if bfe_bytes[0..4] == [0xff, 0xff, 0xff, 0xff] { - skip_top = true; - } else { - bytes.append(&mut bfe_bytes[4..8].to_vec()); - } - } - } - - Ok(bytes[0..length].to_vec()) -} - -/// Verify the UTXO owner's assent to the transaction. -/// This is the rust reference implementation, but the version of -/// this logic that is proven is `lock_script`. -/// -/// This function mocks proof verification. -pub fn std_lockscript_reference_verify_unlock( - spending_lock: Digest, - _bind_to: Digest, - witness_data: [BFieldElement; DIGEST_LENGTH], -) -> bool { - spending_lock == Digest::new(witness_data).hash::() -} - -impl SpendingKey { - pub fn to_address(&self) -> ReceivingAddress { - let randomness: [u8; 32] = shake256::<32>(&bincode::serialize(&self.seed).unwrap()); +impl GenerationSpendingKey { + pub fn to_address(&self) -> GenerationReceivingAddress { + let randomness: [u8; 32] = common::shake256::<32>(&bincode::serialize(&self.seed).unwrap()); let (_sk, pk) = lattice::kem::keygen(randomness); let privacy_digest = self.privacy_preimage.hash::(); - ReceivingAddress { + GenerationReceivingAddress { receiver_identifier: self.receiver_identifier, encryption_key: pk, privacy_digest, @@ -157,68 +70,14 @@ impl SpendingKey { } } - /// Return announces a list of (addition record, utxo, sender randomness, receiver preimage) - pub fn scan_for_announced_utxos( - &self, - transaction: &Transaction, - ) -> Vec<(AdditionRecord, Utxo, Digest, Digest)> { - let mut received_utxos_with_randomnesses = vec![]; - - // for all public scripts that contain a ciphertext for me, - for matching_announcement in transaction - .kernel - .public_announcements - .iter() - .filter(|pa| public_announcement_is_marked(pa)) - .filter(|pa| { - let receiver_id = receiver_identifier_from_public_announcement(pa); - match receiver_id { - Ok(recid) => recid == self.receiver_identifier, - Err(_) => false, - } - }) - { - // decrypt it to obtain the utxo and sender randomness - let ciphertext = ciphertext_from_public_announcement(matching_announcement); - let decryption_result = match ciphertext { - Ok(ctxt) => self.decrypt(&ctxt), - _ => { - continue; - } - }; - let (utxo, sender_randomness) = match decryption_result { - Ok(tuple) => tuple, - _ => { - continue; - } - }; - - // and join those with the receiver digest to get a commitment - // Note: the commitment is computed in the same way as in the mutator set. - let receiver_preimage = self.privacy_preimage; - let receiver_digest = receiver_preimage.hash::(); - let addition_record = commit(Hash::hash(&utxo), sender_randomness, receiver_digest); - - // push to list - received_utxos_with_randomnesses.push(( - addition_record, - utxo, - sender_randomness, - receiver_preimage, - )); - } - - received_utxos_with_randomnesses - } - pub fn derive_from_seed(seed: Digest) -> Self { let privacy_preimage = Hash::hash_varlen(&[seed.values().to_vec(), vec![BFieldElement::new(0)]].concat()); let unlock_key = Hash::hash_varlen(&[seed.values().to_vec(), vec![BFieldElement::new(1)]].concat()); - let randomness: [u8; 32] = shake256::<32>(&bincode::serialize(&seed).unwrap()); + let randomness: [u8; 32] = common::shake256::<32>(&bincode::serialize(&seed).unwrap()); let (sk, _pk) = lattice::kem::keygen(randomness); - let receiver_identifier = derive_receiver_id(seed); + let receiver_identifier = common::derive_receiver_id(seed); let spending_key = Self { receiver_identifier, @@ -233,7 +92,7 @@ impl SpendingKey { let receiving_address = spending_key.to_address(); let encoded_address = receiving_address.to_bech32m(Network::Alpha).unwrap(); let decoded_address = - ReceivingAddress::from_bech32m(&encoded_address, Network::Alpha).unwrap(); + GenerationReceivingAddress::from_bech32m(&encoded_address, Network::Alpha).unwrap(); assert_eq!( receiving_address, decoded_address, "encoding/decoding from bech32m must succeed. Receiving address was: {receiving_address:#?}" @@ -243,7 +102,7 @@ impl SpendingKey { } /// Decrypt a Generation Address ciphertext - fn decrypt(&self, ciphertext: &[BFieldElement]) -> Result<(Utxo, Digest)> { + pub(super) fn decrypt(&self, ciphertext: &[BFieldElement]) -> Result<(Utxo, Digest)> { // parse ciphertext if ciphertext.len() <= CIPHERTEXT_SIZE_IN_BFES { bail!("Ciphertext does not have nonce."); @@ -263,7 +122,7 @@ impl SpendingKey { let cipher = Aes256Gcm::new(&shared_key.into()); let nonce_as_bytes = [nonce_ctxt[0].value().to_be_bytes().to_vec(), vec![0u8; 4]].concat(); let nonce = Nonce::from_slice(&nonce_as_bytes); // almost 64 bits; unique per message - let ciphertext_bytes = bfes_to_bytes(dem_ctxt)?; + let ciphertext_bytes = common::bfes_to_bytes(dem_ctxt)?; let plaintext = match cipher.decrypt(nonce, ciphertext_bytes.as_ref()) { Ok(ptxt) => ptxt, Err(_) => bail!("Failed to decrypt symmetric payload."), @@ -276,20 +135,13 @@ impl SpendingKey { fn generate_spending_lock(&self) -> Digest { self.unlock_key.hash::() } - - /// Unlock the UTXO binding it to some transaction by its kernel hash. - /// This function mocks proof generation. - pub fn binding_unlock(&self, _bind_to: Digest) -> [BFieldElement; DIGEST_LENGTH] { - let witness_data = self.unlock_key; - witness_data.values() - } } -impl ReceivingAddress { - pub fn from_spending_key(spending_key: &SpendingKey) -> Self { +impl GenerationReceivingAddress { + pub fn from_spending_key(spending_key: &GenerationSpendingKey) -> Self { let seed = spending_key.seed; - let receiver_identifier = derive_receiver_id(seed); - let randomness: [u8; 32] = shake256::<32>(&bincode::serialize(&seed).unwrap()); + let receiver_identifier = common::derive_receiver_id(seed); + let randomness: [u8; 32] = common::shake256::<32>(&bincode::serialize(&seed).unwrap()); let (_sk, pk) = lattice::kem::keygen(randomness); let privacy_digest = spending_key.privacy_preimage.hash::(); Self { @@ -301,7 +153,7 @@ impl ReceivingAddress { } pub fn derive_from_seed(seed: Digest) -> Self { - let spending_key = SpendingKey::derive_from_seed(seed); + let spending_key = GenerationSpendingKey::derive_from_seed(seed); Self::from_spending_key(&spending_key) } @@ -335,7 +187,7 @@ impl ReceivingAddress { Ok(ctxt) => ctxt, Err(_) => bail!("Could not encrypt payload."), }; - let ciphertext_bfes = bytes_to_bfes(&ciphertext); + let ciphertext_bfes = common::bytes_to_bfes(&ciphertext); // concatenate and return Ok([ @@ -346,42 +198,7 @@ impl ReceivingAddress { .concat()) } - /// Generate a public announcement, which is a ciphertext only the - /// recipient can decrypt, along with a pubscript that reads - /// some input of that length. - pub fn generate_public_announcement( - &self, - utxo: &Utxo, - sender_randomness: Digest, - ) -> Result { - let mut ciphertext = vec![GENERATION_FLAG, self.receiver_identifier]; - ciphertext.append(&mut self.encrypt(utxo, sender_randomness)?); - - Ok(PublicAnnouncement::new(ciphertext)) - } - - /// Generate a lock script from the spending lock. Satisfaction - /// of this lock script establishes the UTXO owner's assent to - /// the transaction. The logic contained in here should be - /// identical to `verify_unlock`. - pub fn lock_script(&self) -> LockScript { - let mut push_spending_lock_digest_to_stack = vec![]; - for elem in self.spending_lock.values().iter().rev() { - push_spending_lock_digest_to_stack.push(triton_instr!(push elem.value())); - } - - let instructions = triton_asm!( - divine 5 - hash - {&push_spending_lock_digest_to_stack} - assert_vector - read_io 5 - halt - ); - - instructions.into() - } - + /// returns human readable prefix (hrp) of an address. fn get_hrp(network: Network) -> String { // NOLGA: Neptune lattice-based generation address let mut hrp = "nolga".to_string(); @@ -423,197 +240,16 @@ impl ReceivingAddress { } } - /// Verify the UTXO owner's assent to the transaction. - /// This is the rust reference implementation, but the version of - /// this logic that is proven is `lock_script`. + /// generates a lock script from the spending lock. /// - /// This function mocks proof verification. - fn _reference_verify_unlock( - &self, - msg: Digest, - witness_data: [BFieldElement; DIGEST_LENGTH], - ) -> bool { - std_lockscript_reference_verify_unlock(self.spending_lock, msg, witness_data) - } -} - -// note: copied from twenty_first::math::lattice::kem::shake256() -// which is not public -fn shake256(randomness: impl AsRef<[u8]>) -> [u8; NUM_OUT_BYTES] { - let mut hasher = Shake256::default(); - hasher.update(randomness.as_ref()); - - let mut result = [0u8; NUM_OUT_BYTES]; - hasher.finalize_xof_into(&mut result); - result -} - -/// -/// Claim -/// - (input: Hash(kernel), output: [], program: lock_script) - -#[cfg(test)] -mod test_generation_addresses { - use rand::{random, thread_rng, Rng, RngCore}; - use twenty_first::{math::tip5::Digest, util_types::algebraic_hasher::AlgebraicHasher}; - - use crate::{ - config_models::network::Network, - models::blockchain::{ - shared::Hash, transaction::utxo::Utxo, type_scripts::neptune_coins::NeptuneCoins, - }, - tests::shared::make_mock_transaction, - }; - - use super::*; - - #[test] - fn test_conversion_fixed_length() { - let mut rng = thread_rng(); - const N: usize = 23; - let byte_array: [u8; N] = rng.gen(); - let byte_vec = byte_array.to_vec(); - let bfes = bytes_to_bfes(&byte_vec); - let bytes_again = bfes_to_bytes(&bfes).unwrap(); - - assert_eq!(byte_vec, bytes_again); - } - - #[test] - fn test_conversion_variable_length() { - let mut rng = thread_rng(); - for _ in 0..1000 { - let n: usize = rng.gen_range(0..101); - let mut byte_vec: Vec = vec![0; n]; - rng.try_fill_bytes(&mut byte_vec).unwrap(); - let bfes = bytes_to_bfes(&byte_vec); - let bytes_again = bfes_to_bytes(&bfes).unwrap(); - - assert_eq!(byte_vec, bytes_again); - } - } - - #[test] - fn test_conversion_cornercases() { - for test_case in [ - vec![], - vec![0u8], - vec![0u8, 0u8], - vec![0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8], - vec![0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8], - vec![0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8], - vec![1u8], - 0xffffffff00000000u64.to_be_bytes().to_vec(), - 0xffffffff00000001u64.to_be_bytes().to_vec(), - 0xffffffff00000123u64.to_be_bytes().to_vec(), - 0xffffffffffffffffu64.to_be_bytes().to_vec(), - [ - 0xffffffffffffffffu64.to_be_bytes().to_vec(), - 0xffffffffffffffffu64.to_be_bytes().to_vec(), - ] - .concat(), - ] { - let bfes = bytes_to_bfes(&test_case); - let bytes_again = bfes_to_bytes(&bfes).unwrap(); - - assert_eq!(test_case, bytes_again); - } - } - - #[test] - fn test_keygen_sign_verify() { - let mut rng = thread_rng(); - let seed: Digest = rng.gen(); - let spending_key = SpendingKey::derive_from_seed(seed); - let receiving_address = ReceivingAddress::derive_from_seed(seed); - - let msg: Digest = rng.gen(); - let witness_data = spending_key.binding_unlock(msg); - assert!(receiving_address._reference_verify_unlock(msg, witness_data)); - - let receiving_address_again = spending_key.to_address(); - assert_eq!(receiving_address, receiving_address_again); - } - - #[test] - fn test_bech32m_conversion() { - for _ in 0..100 { - let seed: Digest = thread_rng().gen(); - let receiving_address = ReceivingAddress::derive_from_seed(seed); - let encoded = receiving_address.to_bech32m(Network::Testnet).unwrap(); - let receiving_address_again = - ReceivingAddress::from_bech32m(&encoded, Network::Testnet).unwrap(); - - assert_eq!(receiving_address, receiving_address_again); - } - } - - #[test] - fn test_encrypt_decrypt() { - let mut rng = thread_rng(); - let seed: Digest = rng.gen(); - let spending_key = SpendingKey::derive_from_seed(seed); - let receiving_address = ReceivingAddress::from_spending_key(&spending_key); - - let amount = NeptuneCoins::new(rng.gen_range(0..42000000)); - let coins = amount.to_native_coins(); - let lock_script = receiving_address.lock_script(); - let utxo = Utxo::new(lock_script, coins); - - let sender_randomness: Digest = rng.gen(); - - let ciphertext = receiving_address.encrypt(&utxo, sender_randomness).unwrap(); - println!("ciphertext.get_size() = {}", ciphertext.len() * 8); - - let (utxo_again, sender_randomness_again) = spending_key.decrypt(&ciphertext).unwrap(); - - assert_eq!(utxo, utxo_again); - - assert_eq!(sender_randomness, sender_randomness_again); + /// Satisfaction of this lock script establishes the UTXO owner's assent to + /// the transaction. + pub fn lock_script(&self) -> LockScript { + common::lock_script(self.spending_lock) } - #[test] - fn scan_for_announced_utxos_test() { - // Mark a transaction as containing a generation address, and then verify that - // this is recognized by the receiver. - let mut rng = thread_rng(); - let seed: Digest = rng.gen(); - let spending_key = SpendingKey::derive_from_seed(seed); - let receiving_address = ReceivingAddress::from_spending_key(&spending_key); - let utxo = Utxo { - lock_script_hash: receiving_address.lock_script().hash(), - coins: NeptuneCoins::new(10).to_native_coins(), - }; - let sender_randomness: Digest = random(); - - let public_announcement = receiving_address - .generate_public_announcement(&utxo, sender_randomness) - .unwrap(); - let mut mock_tx = make_mock_transaction(vec![], vec![]); - - assert!(spending_key.scan_for_announced_utxos(&mock_tx).is_empty()); - - // Add a pubscript for our keys and verify that they are recognized - assert!(public_announcement_is_marked(&public_announcement)); - mock_tx - .kernel - .public_announcements - .push(public_announcement); - - let announced_txs = spending_key.scan_for_announced_utxos(&mock_tx); - assert_eq!(1, announced_txs.len()); - - let (read_ar, read_utxo, read_sender_randomness, returned_receiver_preimage) = - announced_txs[0].clone(); - assert_eq!(utxo, read_utxo); - - let expected_addition_record = commit( - Hash::hash(&utxo), - sender_randomness, - receiving_address.privacy_digest, - ); - assert_eq!(expected_addition_record, read_ar); - assert_eq!(sender_randomness, read_sender_randomness); - assert_eq!(returned_receiver_preimage, spending_key.privacy_preimage); + /// returns the privacy digest + pub fn privacy_digest(&self) -> Digest { + self.privacy_digest } } diff --git a/src/models/state/wallet/address/symmetric_key.rs b/src/models/state/wallet/address/symmetric_key.rs new file mode 100644 index 00000000..e02842ab --- /dev/null +++ b/src/models/state/wallet/address/symmetric_key.rs @@ -0,0 +1,187 @@ +//! provides a symmetric key interface based on aes-256-gcm for sending and claiming [Utxo] + +use super::common; +use crate::models::blockchain::shared::Hash; +use crate::models::blockchain::transaction::utxo::LockScript; +use crate::models::blockchain::transaction::utxo::Utxo; +use crate::prelude::twenty_first; +use aead::Aead; +use aead::Key; +use aead::KeyInit; +use aes_gcm::Aes256Gcm; +use aes_gcm::Nonce; +use rand::thread_rng; +use rand::Rng; +use serde::Deserialize; +use serde::Serialize; +use twenty_first::math::b_field_element::BFieldElement; +use twenty_first::math::tip5::Digest; +use twenty_first::util_types::algebraic_hasher::AlgebraicHasher; + +/// represents a symmetric key decryption error +#[derive(Debug, thiserror::Error)] +pub enum DecryptError { + #[error("invalid input to decrypt. ciphertext array is missing the nonce field")] + MissingNonce, + + #[error(transparent)] + ByteConversionFailed(#[from] anyhow::Error), + + #[error("decryption failed")] + DecryptionFailed(#[from] aead::Error), + + #[error("deserialization failed")] + DeserializationFailed(#[from] bincode::Error), +} + +/// represents a symmetric key encryption error +#[derive(Debug, thiserror::Error)] +pub enum EncryptError { + #[error("encryption failed")] + EncryptionFailed(#[from] aead::Error), + + #[error("serialization failed")] + SerializationFailed(#[from] bincode::Error), +} + +/// This uniquely identifies the type field of a PublicAnnouncement. +/// it must not conflict with another type. +pub(super) const SYMMETRIC_KEY_FLAG_U8: u8 = 80; +pub const SYMMETRIC_KEY_FLAG: BFieldElement = BFieldElement::new(SYMMETRIC_KEY_FLAG_U8 as u64); + +/// represents an AES 256 bit symmetric key +/// +/// this is an opaque type. all fields are read-only via accessor methods. +/// +/// implementation note: +/// +/// Presently `SymmetricKey` holds only the seed value. All other values are +/// derived on as-needed (lazy) basis. This is memory efficient and cheap to +/// create a key, but may not be CPU efficient if duplicate operations are +/// performed with the same key. +/// +/// The alternative would be to pre-calculate the various values at +/// creation-time and store them in the struct. This has a higher up-front cost +/// to perform the necessary hashing and a higher memory usage but it quickly +/// becomes worth it when amortized over multiple operations. +/// +/// a hybrid (cache-on-first-use) approach may be feasible, but would require +/// that accessor methods accept &mut self which may not be acceptable. +/// +/// The implementation can be easily changed later if needed as the type is +/// opaque. +#[derive(Clone, Debug, Copy, Serialize, Deserialize, PartialEq, Eq)] +pub struct SymmetricKey { + seed: Digest, +} + +impl SymmetricKey { + /// instantiate `SymmetricKey` from a random seed + pub fn from_seed(seed: Digest) -> Self { + Self { seed } + } + + /// returns the secret key + pub fn secret_key(&self) -> Key { + common::shake256::<32>( + &bincode::serialize(&self.seed).expect("serialization should always succeed"), + ) + .into() + } + + /// returns the privacy preimage + pub fn privacy_preimage(&self) -> Digest { + Hash::hash_varlen(&[&self.seed.values(), [BFieldElement::new(0)].as_slice()].concat()) + } + + /// returns the privacy digest which is a hash of the privacy_preimage + pub fn privacy_digest(&self) -> Digest { + self.privacy_preimage().hash::() + } + + /// returns the receiver_identifier, a public fingerprint + pub fn receiver_identifier(&self) -> BFieldElement { + common::derive_receiver_id(self.seed) + } + + /// decrypt a ciphertext into utxo secrets (utxo, sender_randomness) + /// + /// The ciphertext_bfes param must contain the nonce in the first + /// field and the ciphertext in the remaining fields. + /// + /// The output of `encrypt()` should be used as the input to `decrypt()`. + pub fn decrypt( + &self, + ciphertext_bfes: &[BFieldElement], + ) -> Result<(Utxo, Digest), DecryptError> { + const NONCE_LEN: usize = 1; + + // 1. separate nonce from ciphertext. + let (nonce_ctxt, ciphertext) = match ciphertext_bfes.len() > NONCE_LEN { + true => ciphertext_bfes.split_at(NONCE_LEN), + false => return Err(DecryptError::MissingNonce), + }; + + // 2. generate Nonce and cyphertext_bytes + let nonce_as_bytes = [&nonce_ctxt[0].value().to_be_bytes(), [0u8; 4].as_slice()].concat(); + let nonce = Nonce::from_slice(&nonce_as_bytes); // almost 64 bits; unique per message + let ciphertext_bytes = common::bfes_to_bytes(ciphertext)?; + + // 3. decypt ciphertext to plaintext + let cipher = Aes256Gcm::new(&self.secret_key()); + let plaintext = cipher.decrypt(nonce, ciphertext_bytes.as_ref())?; + + // 4. deserialize plaintext into (utxo, sender_randomness) + Ok(bincode::deserialize(&plaintext)?) + } + + /// encrypts utxo secrets (utxo, sender_randomness) into ciphertext + /// + /// The output of `encrypt()` should be used as the input to `decrypt()`. + pub fn encrypt( + &self, + utxo: &Utxo, + sender_randomness: Digest, + ) -> Result, EncryptError> { + // 1. init randomness + let mut randomness = [0u8; 32]; + let mut rng = thread_rng(); + rng.fill(&mut randomness); + + // 2. generate random nonce + let nonce_bfe: BFieldElement = rng.gen(); + let nonce_as_bytes = [&nonce_bfe.value().to_be_bytes(), [0u8; 4].as_slice()].concat(); + let nonce = Nonce::from_slice(&nonce_as_bytes); // almost 64 bits; unique per message + + // 3. convert secrets to plaintext bytes + let plaintext = bincode::serialize(&(utxo, sender_randomness))?; + + // 4. encrypt plaintext to symmetric ciphertext bytes + let cipher = Aes256Gcm::new(&self.secret_key()); + let ciphertext = cipher.encrypt(nonce, plaintext.as_ref())?; + + // 5. convert ciphertext bytes to [BFieldElement] + let ciphertext_bfes = common::bytes_to_bfes(&ciphertext); + + // 6. concatenate nonce bfe + ciphertext bfes and return + Ok([&[nonce_bfe], ciphertext_bfes.as_slice()].concat()) + } + + /// returns the unlock key + pub fn unlock_key(&self) -> Digest { + Hash::hash_varlen(&[self.seed.values().to_vec(), vec![BFieldElement::new(1)]].concat()) + } + + /// returns the spending lock which is a hash of unlock_key() + pub fn spending_lock(&self) -> Digest { + self.unlock_key().hash::() + } + + /// generates a lock script from the spending lock. + /// + /// Satisfaction of this lock script establishes the UTXO owner's assent to + /// the transaction. + pub fn lock_script(&self) -> LockScript { + common::lock_script(self.spending_lock()) + } +} diff --git a/src/models/state/wallet/mod.rs b/src/models/state/wallet/mod.rs index 7e5d6600..f9792fe8 100644 --- a/src/models/state/wallet/mod.rs +++ b/src/models/state/wallet/mod.rs @@ -8,6 +8,10 @@ pub mod utxo_notification_pool; pub mod wallet_state; pub mod wallet_status; +use self::address::generation_address; +use self::address::symmetric_key; +use crate::models::blockchain::block::block_height::BlockHeight; +use crate::Hash; use anyhow::{bail, Context, Result}; use bip39::Mnemonic; use itertools::Itertools; @@ -18,20 +22,13 @@ use serde::{Deserialize, Serialize}; use std::fs::{self}; use std::path::{Path, PathBuf}; use tracing::info; +use twenty_first::math::b_field_element::BFieldElement; use twenty_first::math::bfield_codec::BFieldCodec; use twenty_first::math::digest::Digest; use twenty_first::math::x_field_element::XFieldElement; use twenty_first::util_types::algebraic_hasher::AlgebraicHasher; use zeroize::{Zeroize, ZeroizeOnDrop}; -use twenty_first::math::b_field_element::BFieldElement; - -use crate::models::blockchain::block::block_height::BlockHeight; - -use crate::Hash; - -use self::address::generation_address; - pub const WALLET_DIRECTORY: &str = "wallet"; pub const WALLET_SECRET_FILE_NAME: &str = "wallet.dat"; pub const WALLET_OUTGOING_SECRETS_FILE_NAME: &str = "outgoing_randomness.dat"; @@ -186,25 +183,18 @@ impl WalletSecret { Ok((wallet, wallet_secret_file_locations)) } - /// Get the next unused spending key. - /// - /// For now, this always returns key at index 0. In the future it will - /// return key at present counter, and increment the counter. - /// - /// Note that incrementing the counter request &mut self. Many areas of the - /// code presently take a shortcut and use nth_generation_spending_key(0) - /// which takes &self. This will likely require a significant refactor in - /// the future to resolve. + /// derives a generation spending key at `index` /// - /// Whenever possible, it should be preferred to use this method over - /// nth_generation_spending_key(0), to lesson future refactoring headaches. - pub fn next_unused_generation_spending_key(&mut self) -> generation_address::SpendingKey { - self.nth_generation_spending_key(0) - } - - pub fn nth_generation_spending_key(&self, counter: u16) -> generation_address::SpendingKey { + /// note: this is a read-only method and does not modify wallet state. When + /// requesting a new key for purposes of a new wallet receiving address, + /// callers should use [wallet_state::WalletState::next_unused_spending_key()] + /// which takes &mut self. + pub fn nth_generation_spending_key( + &self, + index: u16, + ) -> generation_address::GenerationSpendingKey { assert!( - counter.is_zero(), + index.is_zero(), "For now we only support one generation address per wallet" ); @@ -215,12 +205,59 @@ impl WalletSecret { self.secret_seed.0.encode(), vec![ generation_address::GENERATION_FLAG, - BFieldElement::new(counter.into()), + BFieldElement::new(index.into()), ], ] .concat(), ); - generation_address::SpendingKey::derive_from_seed(key_seed) + generation_address::GenerationSpendingKey::derive_from_seed(key_seed) + } + + /// derives a symmetric key at `index` + /// + /// note: this is a read-only method and does not modify wallet state. When + /// requesting a new key for purposes of a new wallet receiving address, + /// callers should use [wallet_state::WalletState::next_unused_spending_key()] + /// which takes &mut self. + pub fn nth_symmetric_key(&self, index: u64) -> symmetric_key::SymmetricKey { + assert!( + index.is_zero(), + "For now we only support one symmetric key per wallet" + ); + + let key_seed = Hash::hash_varlen( + &[ + self.secret_seed.0.encode(), + vec![symmetric_key::SYMMETRIC_KEY_FLAG, BFieldElement::new(index)], + ] + .concat(), + ); + symmetric_key::SymmetricKey::from_seed(key_seed) + } + + // note: legacy tests were written to call nth_generation_spending_key() + // when requesting a new address. As such, they may be unprepared to mutate + // wallet state. This method enables them to compile while making clear + // it is an improper usage. + // + // [wallet_state::WalletState::next_unused_generation_spending_key()] should be used + #[cfg(test)] + pub fn nth_generation_spending_key_for_tests( + &self, + counter: u16, + ) -> generation_address::GenerationSpendingKey { + self.nth_generation_spending_key(counter) + } + + // note: legacy tests were written to call nth_symmetric_key() + // when requesting a new key. As such, they may be unprepared to mutate + // wallet state. This method enables them to compile while making clear + // it is an improper usage. + // + // [wallet_state::WalletState::next_unused_symmetric_key()] should be used + #[cfg(test)] + pub fn nth_symmetric_key_for_tests(&self, counter: u64) -> symmetric_key::SymmetricKey { + self.nth_symmetric_key(counter) } /// Return the secret key that is used to deterministically generate commitment pseudo-randomness @@ -427,7 +464,7 @@ mod wallet_tests { let mut next_block = genesis_block.clone(); let other_wallet_secret = WalletSecret::new_random(); let other_receiver_address = other_wallet_secret - .nth_generation_spending_key(0) + .nth_generation_spending_key_for_tests(0) .to_address(); for _ in 0..12 { let previous_block = next_block; @@ -473,7 +510,7 @@ mod wallet_tests { mock_genesis_wallet_state(own_wallet_secret.clone(), network).await; let other_wallet_secret = WalletSecret::new_random(); let other_recipient_address = other_wallet_secret - .nth_generation_spending_key(0) + .nth_generation_spending_key_for_tests(0) .to_address(); let mut monitored_utxos = get_monitored_utxos(&own_wallet_state).await; @@ -483,7 +520,7 @@ mod wallet_tests { ); let genesis_block = Block::genesis_block(network); - let own_spending_key = own_wallet_secret.nth_generation_spending_key(0); + let own_spending_key = own_wallet_secret.nth_generation_spending_key_for_tests(0); let own_recipient_address = own_spending_key.to_address(); let (block_1, block_1_coinbase_utxo, block_1_coinbase_sender_randomness) = make_mock_block(&genesis_block, None, own_recipient_address, rng.gen()); @@ -607,7 +644,7 @@ mod wallet_tests { let mut own_wallet_state = mock_genesis_wallet_state(own_wallet_secret, network).await; let own_spending_key = own_wallet_state .wallet_secret - .nth_generation_spending_key(0); + .nth_generation_spending_key_for_tests(0); let genesis_block = Block::genesis_block(network); let (block_1, cb_utxo, cb_output_randomness) = make_mock_block( &genesis_block, @@ -751,8 +788,9 @@ mod wallet_tests { // This block spends two UTXOs and gives us none, so the new balance // becomes 2000 let other_wallet = WalletSecret::new_random(); - let other_wallet_recipient_address = - other_wallet.nth_generation_spending_key(0).to_address(); + let other_wallet_recipient_address = other_wallet + .nth_generation_spending_key_for_tests(0) + .to_address(); assert_eq!( Into::::into(22u64), next_block.kernel.header.height @@ -769,7 +807,7 @@ mod wallet_tests { next_block.kernel.header.height ); - let tx_outputs = vec![TxOutput::fake_announcement( + let tx_outputs = vec![TxOutput::fake_address( Utxo { lock_script_hash: LockScript::anyone_can_spend().hash(), coins: NeptuneCoins::new(200).to_native_coins(), @@ -821,7 +859,7 @@ mod wallet_tests { let mut own_wallet_state = mock_genesis_wallet_state(own_wallet_secret, network).await; let own_spending_key = own_wallet_state .wallet_secret - .nth_generation_spending_key(0); + .nth_generation_spending_key_for_tests(0); let own_address = own_spending_key.to_address(); let genesis_block = Block::genesis_block(network); let premine_wallet = mock_genesis_wallet_state(WalletSecret::devnet_wallet(), network) @@ -845,7 +883,7 @@ mod wallet_tests { let previous_msa = genesis_block.kernel.body.mutator_set_accumulator.clone(); let (mut block_1, _, _) = make_mock_block(&genesis_block, None, own_address, rng.gen()); - let tx_outputs_12_to_other = TxOutput::fake_announcement( + let tx_outputs_12_to_other = TxOutput::fake_address( Utxo { coins: NeptuneCoins::new(12).to_native_coins(), lock_script_hash: own_address.lock_script().hash(), @@ -859,7 +897,7 @@ mod wallet_tests { ), own_address.privacy_digest, ); - let tx_outputs_one_to_other = TxOutput::fake_announcement( + let tx_outputs_one_to_other = TxOutput::fake_address( Utxo { coins: NeptuneCoins::new(1).to_native_coins(), lock_script_hash: own_address.lock_script().hash(), @@ -1039,7 +1077,7 @@ mod wallet_tests { let premine_wallet_spending_key = premine_receiver_global_state .wallet_state .wallet_secret - .nth_generation_spending_key(0); + .nth_generation_spending_key_for_tests(0); let (block_2_b, _, _) = make_mock_block( &block_1, None, @@ -1136,7 +1174,7 @@ mod wallet_tests { "Block must be valid before merging txs" ); - let tx_outputs_six = TxOutput::fake_announcement( + let tx_outputs_six = TxOutput::fake_address( Utxo { coins: NeptuneCoins::new(4).to_native_coins(), lock_script_hash: own_address.lock_script().hash(), @@ -1271,7 +1309,7 @@ mod wallet_tests { #[tokio::test] async fn basic_wallet_secret_functionality_test() { let random_wallet_secret = WalletSecret::new_random(); - let spending_key = random_wallet_secret.nth_generation_spending_key(0); + let spending_key = random_wallet_secret.nth_generation_spending_key_for_tests(0); let _address = spending_key.to_address(); let _sender_randomness = random_wallet_secret .generate_sender_randomness(BFieldElement::new(10).into(), random()); @@ -1300,7 +1338,7 @@ mod wallet_tests { fn get_devnet_wallet_info() { // Helper function/test to print the public key associated with the authority signatures let devnet_wallet = WalletSecret::devnet_wallet(); - let spending_key = devnet_wallet.nth_generation_spending_key(0); + let spending_key = devnet_wallet.nth_generation_spending_key_for_tests(0); let address = spending_key.to_address(); println!( "_authority_wallet address: {}", diff --git a/src/models/state/wallet/utxo_notification_pool.rs b/src/models/state/wallet/utxo_notification_pool.rs index 2dfcccb6..63281ba3 100644 --- a/src/models/state/wallet/utxo_notification_pool.rs +++ b/src/models/state/wallet/utxo_notification_pool.rs @@ -1,4 +1,5 @@ use crate::{ + models::blockchain::transaction::AnnouncedUtxo, prelude::twenty_first, util_types::mutator_set::{addition_record::AdditionRecord, commit}, }; @@ -30,6 +31,24 @@ pub type Credibility = i32; pub const UNRECEIVED_UTXO_NOTIFICATION_THRESHOLD_AGE_IN_SECS: u64 = 28 * 24 * 60 * 60; pub const RECEIVED_UTXO_NOTIFICATION_THRESHOLD_AGE_IN_SECS: u64 = 3 * 24 * 60 * 60; +/// represents utxo and secrets necessary for recipient to claim it. +/// +/// [ExpectedUtxo] is intended for offchain temporary storage of utxos that a +/// wallet sends to itself, eg change outputs. +/// +/// The `ExpectedUtxo` will exist in the local [UtxoNotificationPool] from the +/// time the transaction is sent until it is mined in a block and claimed by the +/// wallet. +/// +/// note that when using `ExpectedUtxo` there is a risk of losing funds because +/// the wallet stores this state on disk and if the associated file(s) are lost +/// then the funds cannot be claimed. +/// +/// an alternative is to use onchain symmetric keys instead, which uses some +/// blockchain space and may leak some privacy if a key is ever used more than +/// once. +/// +/// see [UtxoNotificationPool], [AnnouncedUtxo], [UtxoNotification](crate::models::blockchain::transaction::UtxoNotification) #[derive(Clone, Debug, PartialEq, Eq, Hash, GetSize)] pub struct ExpectedUtxo { pub utxo: Utxo, @@ -168,23 +187,16 @@ impl UtxoNotificationPool { /// Scan the transaction for outputs that match with list of expected /// incoming UTXOs, and returns expected UTXOs that are present in the /// transaction. - /// Returns a list of (addition record, UTXO, sender randomness, receiver_preimage) - pub fn scan_for_expected_utxos( - &self, - transaction: &Transaction, - ) -> Vec<(AdditionRecord, Utxo, Digest, Digest)> { - let mut received_expected_utxos = vec![]; - for tx_output in transaction.kernel.outputs.iter() { - if let Some(expected_utxo) = self.notifications.get(tx_output) { - received_expected_utxos.push(( - tx_output.to_owned(), - expected_utxo.utxo.to_owned(), - expected_utxo.sender_randomness, - expected_utxo.receiver_preimage, - )); - } - } - received_expected_utxos + /// Returns an iterator of [AnnouncedUtxo]. (addition record, UTXO, sender randomness, receiver_preimage) + pub fn scan_for_expected_utxos<'a>( + &'a self, + transaction: &'a Transaction, + ) -> impl Iterator + 'a { + transaction.kernel.outputs.iter().filter_map(|tx_output| { + self.notifications + .get(tx_output) + .map(|expected_utxo| expected_utxo.into()) + }) } /// Return all expected UTXOs @@ -403,8 +415,9 @@ mod wallet_state_tests { let mock_tx_containing_expected_utxo = make_mock_transaction(vec![], vec![expected_addition_record]); - let ret_with_tx_containing_utxo = - notification_pool.scan_for_expected_utxos(&mock_tx_containing_expected_utxo); + let ret_with_tx_containing_utxo = notification_pool + .scan_for_expected_utxos(&mock_tx_containing_expected_utxo) + .collect_vec(); assert_eq!(1, ret_with_tx_containing_utxo.len()); // Call scan but with another input. Verify that it returns the empty list @@ -414,7 +427,9 @@ mod wallet_state_tests { receiver_preimage.hash::(), ); let tx_without_utxo = make_mock_transaction(vec![], vec![another_addition_record]); - let ret_with_tx_without_utxo = notification_pool.scan_for_expected_utxos(&tx_without_utxo); + let ret_with_tx_without_utxo = notification_pool + .scan_for_expected_utxos(&tx_without_utxo) + .collect_vec(); assert!(ret_with_tx_without_utxo.is_empty()); // Verify that we can remove the expected UTXO again diff --git a/src/models/state/wallet/wallet_state.rs b/src/models/state/wallet/wallet_state.rs index 61b7feb3..5234695e 100644 --- a/src/models/state/wallet/wallet_state.rs +++ b/src/models/state/wallet/wallet_state.rs @@ -1,5 +1,4 @@ -use super::address::generation_address::SpendingKey; -use super::address::SpendingKeyType; +use super::address::{generation_address, symmetric_key, KeyType, SpendingKey}; use super::coin_with_possible_timelock::CoinWithPossibleTimeLock; use super::rusty_wallet_database::RustyWalletDatabase; use super::utxo_notification_pool::{UtxoNotificationPool, UtxoNotifier}; @@ -12,7 +11,7 @@ use crate::database::storage::storage_vec::traits::*; use crate::database::NeptuneLevelDb; use crate::models::blockchain::block::Block; use crate::models::blockchain::transaction::utxo::Utxo; -use crate::models::blockchain::transaction::{Transaction, TxInput, TxInputList}; +use crate::models::blockchain::transaction::{AnnouncedUtxo, Transaction, TxInput, TxInputList}; use crate::models::blockchain::type_scripts::native_currency::NativeCurrency; use crate::models::blockchain::type_scripts::neptune_coins::NeptuneCoins; use crate::models::consensus::tasm::program::ConsensusProgram; @@ -198,7 +197,7 @@ impl WalletState { // outputs. if sync_label == Digest::default() { // Check if we are premine recipients - let own_spending_key = wallet_state.wallet_secret.nth_generation_spending_key(0); + let own_spending_key = wallet_state.next_unused_spending_key(KeyType::Generation); let own_receiving_address = own_spending_key.to_address(); for utxo in Block::premine_utxos(cli_args.network) { if utxo.lock_script_hash == own_receiving_address.lock_script().hash() { @@ -207,13 +206,14 @@ impl WalletState { .add_expected_utxo( utxo, Digest::default(), - own_spending_key.privacy_preimage, + own_spending_key.privacy_preimage(), UtxoNotifier::Premine, ) .unwrap(); } } + // note: this will write modified state to disk. wallet_state .update_wallet_state_with_new_block( &MutatorSetAccumulator::default(), @@ -257,51 +257,59 @@ impl WalletState { spent_own_utxos } - /// Scan the given transaction for announced UTXOs as - /// recognized by owned `SpendingKey`s, and then verify - /// those announced UTXOs are actually present. - fn scan_for_announced_utxos( - &self, - transaction: &Transaction, - ) -> Vec<(AdditionRecord, Utxo, Digest, Digest)> { - // TODO: These spending keys should probably be derived dynamically from some - // state in the wallet. And we should allow for other types than just generation - // addresses. - let spending_keys = self.get_known_generation_spending_keys(); - - // get recognized UTXOs - let recognized_utxos = spending_keys - .iter() - .map(|spending_key| spending_key.scan_for_announced_utxos(transaction)) - .collect_vec() - .concat(); - - // filter for presence in transaction - recognized_utxos + /// Scan the given transaction for announced UTXOs as recognized by owned + /// `SpendingKey`s, and then verify those announced UTXOs are actually + /// present. + fn scan_for_announced_utxos<'a>( + &'a self, + transaction: &'a Transaction, + ) -> impl Iterator + 'a { + // scan for announced utxos for every known key of every key type. + self.get_all_known_spending_keys() .into_iter() - .filter(|(ar, ut, _sr, _rp)| if !transaction.kernel.outputs.contains(ar) { - warn!("Transaction does not contain announced UTXO encrypted to own receiving address. Announced UTXO was: {ut:#?}"); - false - } else { true }) - .collect_vec() + .flat_map(|key| key.scan_for_announced_utxos(transaction).collect_vec()) + + // filter for presence in transaction + // + // note: this is a nice sanity check, but probably is un-necessary + // work that can eventually be removed. + .filter(|au| match transaction.kernel.outputs.contains(&au.addition_record) { + true => true, + false => { + warn!("Transaction does not contain announced UTXO encrypted to own receiving address. Announced UTXO was: {:#?}", au.utxo); + false + } + }) } // returns true if the utxo can be unlocked by one of the // known wallet keys. pub fn can_unlock(&self, utxo: &Utxo) -> bool { - self.get_known_generation_spending_keys() - .iter() - .map(|k| k.to_address().lock_script().hash()) - .any(|h| h == utxo.lock_script_hash) + self.find_spending_key_for_utxo(utxo).is_some() } // returns Some(SpendingKeyType) if the utxo can be unlocked by one of the known // wallet keys. - pub fn find_spending_key_for_utxo(&self, utxo: &Utxo) -> Option { - self.get_known_generation_spending_keys() - .iter() + pub fn find_spending_key_for_utxo(&self, utxo: &Utxo) -> Option { + self.get_all_known_spending_keys() + .into_iter() .find(|k| k.to_address().lock_script().hash() == utxo.lock_script_hash) - .map(|k| (*k).into()) + } + + /// returns all spending keys of all key types with derivation index less than current counter + pub fn get_all_known_spending_keys(&self) -> Vec { + KeyType::all_types() + .into_iter() + .flat_map(|key_type| self.get_known_spending_keys(key_type)) + .collect() + } + + /// returns all spending keys of `key_type` with derivation index less than current counter + pub fn get_known_spending_keys(&self, key_type: KeyType) -> Vec { + match key_type { + KeyType::Generation => self.get_known_generation_spending_keys(), + KeyType::Symmetric => self.get_known_symmetric_keys(), + } } // TODO: These spending keys should probably be derived dynamically from some @@ -313,9 +321,61 @@ impl WalletState { // funds. We could also perform a sequential scan at startup (or import) // of keys that have received funds, up to some "gap". In bitcoin/bip32 // this gap is defined as 20 keys in a row that have never received funds. - pub fn get_known_generation_spending_keys(&self) -> Vec { + fn get_known_generation_spending_keys(&self) -> Vec { // for now we always return just the 1st key. - vec![self.wallet_secret.nth_generation_spending_key(0)] + vec![self.wallet_secret.nth_generation_spending_key(0).into()] + } + + // TODO: These spending keys should probably be derived dynamically from some + // state in the wallet. And we should allow for other types than just generation + // addresses. + // + // Probably the wallet should keep track of index of latest derived key + // that has been requested by the user for purpose of receiving + // funds. We could also perform a sequential scan at startup (or import) + // of keys that have received funds, up to some "gap". In bitcoin/bip32 + // this gap is defined as 20 keys in a row that have never received funds. + fn get_known_symmetric_keys(&self) -> Vec { + // for now we always return just the 1st key. + vec![self.wallet_secret.nth_symmetric_key(0).into()] + } + + /// Get the next unused spending key of a given type. + /// + /// For now, this always returns key at index 0. In the future it will + /// return key at present counter (for key_type), and increment the counter. + /// + /// Note that incrementing the counter requires &mut self. + /// + /// Note that incrementing the counter modifies wallet state. It is + /// important to write to disk afterward to avoid possible funds loss. + pub fn next_unused_spending_key(&mut self, key_type: KeyType) -> SpendingKey { + match key_type { + KeyType::Generation => self.next_unused_generation_spending_key().into(), + KeyType::Symmetric => self.next_unused_symmetric_key().into(), + } + } + + /// Get the next unused generation spending key. + /// + /// For now, this always returns key at index 0. In the future it will + /// return key at present counter, and increment the counter. + /// + /// Note that incrementing the counter modifies wallet state. It is + /// important to write to disk afterward to avoid possible funds loss. + fn next_unused_generation_spending_key(&mut self) -> generation_address::GenerationSpendingKey { + self.wallet_secret.nth_generation_spending_key(0) + } + + /// Get the next unused symmetric key. + /// + /// For now, this always returns key at index 0. In the future it will + /// return key at present counter, and increment the counter. + /// + /// Note that incrementing the counter modifies wallet state. It is + /// important to write to disk afterward to avoid possible funds loss. + pub fn next_unused_symmetric_key(&mut self) -> symmetric_key::SymmetricKey { + self.wallet_secret.nth_symmetric_key(0) } /// Update wallet state with new block. Assume the given block @@ -330,24 +390,33 @@ impl WalletState { let spent_inputs: Vec<(Utxo, AbsoluteIndexSet, u64)> = self.scan_for_spent_utxos(&transaction).await; - // utxo, sender randomness, receiver preimage, addition record - let mut received_outputs: Vec<(AdditionRecord, Utxo, Digest, Digest)> = vec![]; - received_outputs.append(&mut self.scan_for_announced_utxos(&transaction)); - debug!( - "received_outputs as announced outputs = {}", - received_outputs.len() - ); - let expected_utxos_in_this_block = - self.expected_utxos.scan_for_expected_utxos(&transaction); - received_outputs.append(&mut expected_utxos_in_this_block.clone()); - debug!("received total outputs: = {}", received_outputs.len()); + let onchain_received_outputs = self.scan_for_announced_utxos(&transaction); + + let offchain_received_outputs = self + .expected_utxos + .scan_for_expected_utxos(&transaction) + .collect_vec(); + + let all_received_outputs = + onchain_received_outputs.chain(offchain_received_outputs.iter().cloned()); let addition_record_to_utxo_info: HashMap = - received_outputs - .into_iter() - .map(|(ar, utxo, send_rand, rec_premi)| (ar, (utxo, send_rand, rec_premi))) + all_received_outputs + .map(|au| { + ( + au.addition_record, + (au.utxo, au.sender_randomness, au.receiver_preimage), + ) + }) .collect(); + debug!( + "announced outputs received: onchain: {}, offchain: {}, total: {}", + addition_record_to_utxo_info.len() - offchain_received_outputs.len(), + offchain_received_outputs.len(), + addition_record_to_utxo_info.len() + ); + // Derive the membership proofs for received UTXOs, and in // the process update existing membership proofs with // updates from this block @@ -636,18 +705,16 @@ impl WalletState { self.store_utxo_ms_recovery_data(item).await?; } + // Mark all expected UTXOs that were received in this block as received + offchain_received_outputs.into_iter().for_each(|au| { + self.expected_utxos + .mark_as_received(au.addition_record, new_block.hash()) + .expect("Expected UTXO must be present when marking it as received") + }); + self.wallet_db.set_sync_label(new_block.hash()).await; self.wallet_db.persist().await; - // Mark all expected UTXOs that were received in this block as received - expected_utxos_in_this_block - .into_iter() - .for_each(|(addition_rec, _, _, _)| { - self.expected_utxos - .mark_as_received(addition_rec, new_block.hash()) - .expect("Expected UTXO must be present when marking it as received") - }); - Ok(()) } @@ -845,7 +912,7 @@ mod tests { let mut rng = thread_rng(); let network = Network::RegTest; let own_wallet_secret = WalletSecret::new_random(); - let own_spending_key = own_wallet_secret.nth_generation_spending_key(0); + let own_spending_key = own_wallet_secret.nth_generation_spending_key_for_tests(0); let own_global_state_lock = mock_genesis_global_state(network, 0, own_wallet_secret).await; let mut own_global_state = own_global_state_lock.lock_guard_mut().await; let genesis_block = Block::genesis_block(network); @@ -867,7 +934,7 @@ mod tests { // Add two blocks with no UTXOs for us let other_recipient_address = WalletSecret::new_random() - .nth_generation_spending_key(0) + .nth_generation_spending_key_for_tests(0) .to_address(); let mut latest_block = genesis_block; for _ in 1..=2 { diff --git a/src/peer_loop.rs b/src/peer_loop.rs index 4ce494f5..54305ff6 100644 --- a/src/peer_loop.rs +++ b/src/peer_loop.rs @@ -1294,7 +1294,9 @@ mod peer_loop_tests { nonce[2].increment(); different_genesis_block.set_header_nonce(nonce); let a_wallet_secret = WalletSecret::new_random(); - let a_recipient_address = a_wallet_secret.nth_generation_spending_key(0).to_address(); + let a_recipient_address = a_wallet_secret + .nth_generation_spending_key_for_tests(0) + .to_address(); let (block_1_with_different_genesis, _, _) = make_mock_block_with_valid_pow( &different_genesis_block, None, @@ -1376,7 +1378,9 @@ mod peer_loop_tests { // Make a with hash above what the implied threshold from // `target_difficulty` requires let a_wallet_secret = WalletSecret::new_random(); - let a_recipient_address = a_wallet_secret.nth_generation_spending_key(0).to_address(); + let a_recipient_address = a_wallet_secret + .nth_generation_spending_key_for_tests(0) + .to_address(); let (block_without_valid_pow, _, _) = make_mock_block_with_invalid_pow(&genesis_block, None, a_recipient_address, rng.gen()); @@ -1461,7 +1465,9 @@ mod peer_loop_tests { let genesis_block: Block = global_state_mut.chain.archival_state().get_tip().await; let a_wallet_secret = WalletSecret::new_random(); - let a_recipient_address = a_wallet_secret.nth_generation_spending_key(0).to_address(); + let a_recipient_address = a_wallet_secret + .nth_generation_spending_key_for_tests(0) + .to_address(); let (block_1, _, _) = make_mock_block_with_valid_pow(&genesis_block, None, a_recipient_address, rng.gen()); global_state_mut.set_new_tip(block_1.clone()).await?; @@ -1523,7 +1529,9 @@ mod peer_loop_tests { let genesis_block: Block = global_state_mut.chain.archival_state().get_tip().await; let peer_address = get_dummy_socket_address(0); let a_wallet_secret = WalletSecret::new_random(); - let a_recipient_address = a_wallet_secret.nth_generation_spending_key(0).to_address(); + let a_recipient_address = a_wallet_secret + .nth_generation_spending_key_for_tests(0) + .to_address(); let (block_1, _, _) = make_mock_block_with_valid_pow(&genesis_block, None, a_recipient_address, rng.gen()); let (block_2_a, _, _) = @@ -1613,7 +1621,9 @@ mod peer_loop_tests { let genesis_block: Block = global_state_mut.chain.archival_state().get_tip().await; let peer_address = get_dummy_socket_address(0); let a_wallet_secret = WalletSecret::new_random(); - let a_recipient_address = a_wallet_secret.nth_generation_spending_key(0).to_address(); + let a_recipient_address = a_wallet_secret + .nth_generation_spending_key_for_tests(0) + .to_address(); let (block_1, _, _) = make_mock_block_with_valid_pow(&genesis_block, None, a_recipient_address, rng.gen()); let (block_2_a, _, _) = @@ -1677,7 +1687,9 @@ mod peer_loop_tests { let genesis_block: Block = global_state_mut.chain.archival_state().get_tip().await; let peer_address = get_dummy_socket_address(0); let a_wallet_secret = WalletSecret::new_random(); - let a_recipient_address = a_wallet_secret.nth_generation_spending_key(0).to_address(); + let a_recipient_address = a_wallet_secret + .nth_generation_spending_key_for_tests(0) + .to_address(); let (block_1, _, _) = make_mock_block_with_valid_pow(&genesis_block, None, a_recipient_address, rng.gen()); let (block_2_a, _, _) = @@ -1732,7 +1744,9 @@ mod peer_loop_tests { let (_peer_broadcast_tx, from_main_rx_clone, to_main_tx, mut to_main_rx1, state_lock, hsd) = get_test_genesis_setup(network, 0).await?; let a_wallet_secret = WalletSecret::new_random(); - let a_recipient_address = a_wallet_secret.nth_generation_spending_key(0).to_address(); + let a_recipient_address = a_wallet_secret + .nth_generation_spending_key_for_tests(0) + .to_address(); let peer_address = get_dummy_socket_address(0); let genesis_block: Block = state_lock .lock_guard() @@ -1803,7 +1817,9 @@ mod peer_loop_tests { .get_tip() .await; let a_wallet_secret = WalletSecret::new_random(); - let a_recipient_address = a_wallet_secret.nth_generation_spending_key(0).to_address(); + let a_recipient_address = a_wallet_secret + .nth_generation_spending_key_for_tests(0) + .to_address(); let (block_1, _, _) = make_mock_block_with_valid_pow(&genesis_block, None, a_recipient_address, rng.gen()); let (block_2, _, _) = @@ -1886,7 +1902,7 @@ mod peer_loop_tests { let own_recipient_address = global_state_mut .wallet_state .wallet_secret - .nth_generation_spending_key(0) + .nth_generation_spending_key_for_tests(0) .to_address(); let (block_1, _, _) = make_mock_block_with_valid_pow( &genesis_block.clone(), @@ -1979,7 +1995,9 @@ mod peer_loop_tests { let peer_address: SocketAddr = get_dummy_socket_address(0); let genesis_block: Block = global_state_mut.chain.archival_state().get_tip().await; let a_wallet_secret = WalletSecret::new_random(); - let a_recipient_address = a_wallet_secret.nth_generation_spending_key(0).to_address(); + let a_recipient_address = a_wallet_secret + .nth_generation_spending_key_for_tests(0) + .to_address(); let (block_1, _, _) = make_mock_block_with_valid_pow( &genesis_block.clone(), None, @@ -2062,7 +2080,9 @@ mod peer_loop_tests { let genesis_block: Block = global_state.chain.archival_state().get_tip().await; let a_wallet_secret = WalletSecret::new_random(); - let a_recipient_address = a_wallet_secret.nth_generation_spending_key(0).to_address(); + let a_recipient_address = a_wallet_secret + .nth_generation_spending_key_for_tests(0) + .to_address(); let (block_1, _, _) = make_mock_block_with_valid_pow( &genesis_block.clone(), None, @@ -2141,7 +2161,9 @@ mod peer_loop_tests { get_test_genesis_setup(network, 0).await?; let mut global_state_mut = state_lock.lock_guard_mut().await; let a_wallet_secret = WalletSecret::new_random(); - let a_recipient_address = a_wallet_secret.nth_generation_spending_key(0).to_address(); + let a_recipient_address = a_wallet_secret + .nth_generation_spending_key_for_tests(0) + .to_address(); let peer_socket_address: SocketAddr = get_dummy_socket_address(0); let genesis_block: Block = global_state_mut.chain.archival_state().get_tip().await; let (block_1, _, _) = make_mock_block_with_valid_pow( @@ -2251,7 +2273,9 @@ mod peer_loop_tests { let genesis_block: Block = global_state_mut.chain.archival_state().get_tip().await; let a_wallet_secret = WalletSecret::new_random(); - let a_recipient_address = a_wallet_secret.nth_generation_spending_key(0).to_address(); + let a_recipient_address = a_wallet_secret + .nth_generation_spending_key_for_tests(0) + .to_address(); let (block_1, _, _) = make_mock_block_with_valid_pow( &genesis_block.clone(), None, diff --git a/src/rpc_server.rs b/src/rpc_server.rs index b3dd73d2..d752e467 100644 --- a/src/rpc_server.rs +++ b/src/rpc_server.rs @@ -1,3 +1,10 @@ +//! implements an RPC server and client based on [tarpc] +//! +//! at present tarpc clients must also be written in rust. +//! +//! In the future we may want to explore adding an rpc layer that is friendly to +//! other languages. + use crate::config_models::network::Network; use crate::models::blockchain::block::block_header::BlockHeader; use crate::models::blockchain::block::block_height::BlockHeight; @@ -11,7 +18,8 @@ use crate::models::consensus::timestamp::Timestamp; use crate::models::peer::InstanceId; use crate::models::peer::PeerInfo; use crate::models::peer::PeerStanding; -use crate::models::state::wallet::address::ReceivingAddressType; +use crate::models::state::wallet::address::KeyType; +use crate::models::state::wallet::address::ReceivingAddress; use crate::models::state::wallet::coin_with_possible_timelock::CoinWithPossibleTimeLock; use crate::models::state::wallet::wallet_status::WalletStatus; use crate::models::state::GlobalStateLock; @@ -109,7 +117,7 @@ pub trait RPC { async fn wallet_status() -> WalletStatus; /// Return an address that this client can receive funds on - async fn next_receiving_address() -> ReceivingAddressType; + async fn next_receiving_address(key_type: KeyType) -> ReceivingAddress; /// Return the number of transactions in the mempool async fn mempool_tx_count() -> usize; @@ -121,7 +129,7 @@ pub trait RPC { async fn dashboard_overview_data() -> DashBoardOverviewDataFromClient; /// Determine whether the user-supplied string is a valid address - async fn validate_address(address: String, network: Network) -> Option; + async fn validate_address(address: String, network: Network) -> Option; /// Determine whether the user-supplied string is a valid amount async fn validate_amount(amount: String) -> Option; @@ -142,15 +150,47 @@ pub trait RPC { async fn clear_standing_by_ip(ip: IpAddr); /// Send coins to a single recipient. + /// + /// See docs for [send_to_many()](Self::send_to_many()) async fn send( amount: NeptuneCoins, - address: ReceivingAddressType, + address: ReceivingAddress, + owned_utxo_notify_method: UtxoNotifyMethod, fee: NeptuneCoins, ) -> Option; /// Send coins to multiple recipients + /// + /// `outputs` is a list of transaction outputs in the format + /// `[(address:amount)]`. The address may be any type supported by + /// [ReceivingAddressType]. + /// + /// `owned_utxo_notify_method` specifies how our wallet will be notified of + /// any outputs destined for it. This includes the change output if one is + /// necessary. [UtxoNotifyMethod] defines `OnChain` and `OffChain` delivery + /// of notifications. + /// + /// `OffChain` delivery requires less blockchain space and may result in a + /// lower fee than `OnChain` delivery however there is more potential of + /// losing funds should the wallet files become corrupted or lost. + /// + /// tip: if using `OnChain` notification use a + /// [ReceivingAddressType::Symmetric] as the receiving address for any + /// outputs destined for your own wallet. This happens automatically for + /// the Change output only. + /// + /// `fee` represents the fee in native coins to pay the miner who mines + /// the block that initially confirms the resulting transaction. + /// + /// a [Digest] of the resulting [Transaction](crate::models::blockchain::transaction::Transaction) is returned on success, else [None]. + /// + /// todo: shouldn't we return `Transaction` instead? + /// + /// future work: add `unowned_utxo_notify_method` param. + /// see comment for [TxOutput::auto()](crate::models::blockchain::transaction::TxOutput::auto()) async fn send_to_many( - outputs: Vec<(ReceivingAddressType, NeptuneCoins)>, + outputs: Vec<(ReceivingAddress, NeptuneCoins)>, + owned_utxo_notify_method: UtxoNotifyMethod, fee: NeptuneCoins, ) -> Option; @@ -209,10 +249,12 @@ impl NeptuneRPCServer { } impl RPC for NeptuneRPCServer { + // documented in trait. do not add doc-comment. async fn network(self, _: context::Context) -> Network { self.state.cli().network } + // documented in trait. do not add doc-comment. async fn own_listen_address_for_peers(self, _context: context::Context) -> Option { let listen_for_peers_ip = self.state.cli().listen_addr; let listen_for_peers_socket = self.state.cli().peer_port; @@ -220,10 +262,12 @@ impl RPC for NeptuneRPCServer { Some(socket_address) } + // documented in trait. do not add doc-comment. async fn own_instance_id(self, _context: context::Context) -> InstanceId { self.state.lock_guard().await.net.instance_id } + // documented in trait. do not add doc-comment. async fn block_height(self, _: context::Context) -> BlockHeight { self.state .lock_guard() @@ -235,10 +279,12 @@ impl RPC for NeptuneRPCServer { .height } + // documented in trait. do not add doc-comment. async fn confirmations(self, _: context::Context) -> Option { self.confirmations_internal().await } + // documented in trait. do not add doc-comment. async fn utxo_digest(self, _: context::Context, leaf_index: u64) -> Option { let state = self.state.lock_guard().await; let aocl = &state.chain.archival_state().archival_mutator_set.ams().aocl; @@ -249,6 +295,7 @@ impl RPC for NeptuneRPCServer { } } + // documented in trait. do not add doc-comment. async fn block_digest( self, _: context::Context, @@ -264,6 +311,7 @@ impl RPC for NeptuneRPCServer { .map(|_| digest) } + // documented in trait. do not add doc-comment. async fn block_info( self, _: context::Context, @@ -281,6 +329,7 @@ impl RPC for NeptuneRPCServer { )) } + // documented in trait. do not add doc-comment. async fn latest_tip_digests(self, _context: tarpc::context::Context, n: usize) -> Vec { let state = self.state.lock_guard().await; @@ -293,6 +342,7 @@ impl RPC for NeptuneRPCServer { .await } + // documented in trait. do not add doc-comment. async fn peer_info(self, _: context::Context) -> Vec { self.state .lock_guard() @@ -304,7 +354,7 @@ impl RPC for NeptuneRPCServer { .collect() } - #[doc = r" Return info about all peers that have been sanctioned"] + // documented in trait. do not add doc-comment. async fn all_sanctioned_peers( self, _context: tarpc::context::Context, @@ -332,14 +382,14 @@ impl RPC for NeptuneRPCServer { all_sanctions } + // documented in trait. do not add doc-comment. async fn validate_address( self, _ctx: context::Context, address_string: String, network: Network, - ) -> Option { - let ret = if let Ok(address) = ReceivingAddressType::from_bech32m(&address_string, network) - { + ) -> Option { + let ret = if let Ok(address) = ReceivingAddress::from_bech32m(&address_string, network) { Some(address) } else { None @@ -351,6 +401,7 @@ impl RPC for NeptuneRPCServer { ret } + // documented in trait. do not add doc-comment. async fn validate_amount( self, _ctx: context::Context, @@ -367,6 +418,7 @@ impl RPC for NeptuneRPCServer { Some(amount) } + // documented in trait. do not add doc-comment. async fn amount_leq_synced_balance(self, _ctx: context::Context, amount: NeptuneCoins) -> bool { let now = Timestamp::now(); // test inequality @@ -379,6 +431,7 @@ impl RPC for NeptuneRPCServer { amount <= wallet_status.synced_unspent_available_amount(now) } + // documented in trait. do not add doc-comment. async fn synced_balance(self, _context: tarpc::context::Context) -> NeptuneCoins { let now = Timestamp::now(); let wallet_status = self @@ -390,6 +443,7 @@ impl RPC for NeptuneRPCServer { wallet_status.synced_unspent_available_amount(now) } + // documented in trait. do not add doc-comment. async fn wallet_status(self, _context: tarpc::context::Context) -> WalletStatus { self.state .lock_guard() @@ -398,6 +452,7 @@ impl RPC for NeptuneRPCServer { .await } + // documented in trait. do not add doc-comment. async fn header( self, _context: tarpc::context::Context, @@ -415,28 +470,37 @@ impl RPC for NeptuneRPCServer { // future: this should perhaps take a param indicating what type // of receiving address. for now we just use/assume // a Generation address. + // + // documented in trait. do not add doc-comment. async fn next_receiving_address( self, _context: tarpc::context::Context, - ) -> ReceivingAddressType { - self.state - .lock_guard_mut() - .await + key_type: KeyType, + ) -> ReceivingAddress { + let mut global_state_mut = self.state.lock_guard_mut().await; + + let address = global_state_mut .wallet_state - .wallet_secret - .next_unused_generation_spending_key() - .to_address() - .into() + .next_unused_spending_key(key_type) + .to_address(); + + // persist wallet state to disk + global_state_mut.persist_wallet().await.expect("flushed"); + + address } + // documented in trait. do not add doc-comment. async fn mempool_tx_count(self, _context: tarpc::context::Context) -> usize { self.state.lock_guard().await.mempool.len() } + // documented in trait. do not add doc-comment. async fn mempool_size(self, _context: tarpc::context::Context) -> usize { self.state.lock_guard().await.mempool.get_size() } + // documented in trait. do not add doc-comment. async fn history( self, _context: tarpc::context::Context, @@ -454,6 +518,7 @@ impl RPC for NeptuneRPCServer { display_history } + // documented in trait. do not add doc-comment. async fn dashboard_overview_data( self, _context: tarpc::context::Context, @@ -491,8 +556,10 @@ impl RPC for NeptuneRPCServer { } /******** CHANGE THINGS ********/ - /// Locking: - /// * acquires `global_state_lock` for write + // Locking: + // * acquires `global_state_lock` for write + // + // documented in trait. do not add doc-comment. async fn clear_all_standings(self, _: context::Context) { let mut global_state_mut = self.state.lock_guard_mut().await; global_state_mut @@ -512,8 +579,10 @@ impl RPC for NeptuneRPCServer { .expect("flushed DBs"); } - /// Locking: - /// * acquires `global_state_lock` for write + // Locking: + // * acquires `global_state_lock` for write + // + // documented in trait. do not add doc-comment. async fn clear_standing_by_ip(self, _: context::Context, ip: IpAddr) { let mut global_state_mut = self.state.lock_guard_mut().await; global_state_mut @@ -535,44 +604,48 @@ impl RPC for NeptuneRPCServer { .expect("flushed DBs"); } + // documented in trait. do not add doc-comment. async fn send( self, ctx: context::Context, amount: NeptuneCoins, - address: ReceivingAddressType, + address: ReceivingAddress, + owned_utxo_notify_method: UtxoNotifyMethod, fee: NeptuneCoins, ) -> Option { - self.send_to_many(ctx, vec![(address, amount)], fee).await + self.send_to_many(ctx, vec![(address, amount)], owned_utxo_notify_method, fee) + .await } - /// Locking: - /// * acquires `global_state_lock` for write - /// + // Locking: + // * acquires `global_state_lock` for write + // // TODO: add an endpoint to get recommended fee density. + // + // documented in trait. do not add doc-comment. async fn send_to_many( self, _ctx: context::Context, - outputs: Vec<(ReceivingAddressType, NeptuneCoins)>, + outputs: Vec<(ReceivingAddress, NeptuneCoins)>, + owned_utxo_notify_method: UtxoNotifyMethod, fee: NeptuneCoins, ) -> Option { let span = tracing::debug_span!("Constructing transaction"); let _enter = span.enter(); let now = Timestamp::now(); - // we obtain a change_address first, as it requires modifying wallet state. - let change_spending_key = self - .state - .lock_guard_mut() - .await - .wallet_state - .wallet_secret - .next_unused_generation_spending_key(); + // obtain next unused symmetric key for change utxo + let change_key = { + let mut s = self.state.lock_guard_mut().await; + let key = s.wallet_state.next_unused_spending_key(KeyType::Symmetric); - // write state to disk, as create_transaction() may take a long time. - self.state.flush_databases().await.expect("flushed DBs"); + // write state to disk. create_transaction() may be slow. + s.persist_wallet().await.expect("flushed"); + key + }; let state = self.state.lock_guard().await; - let mut tx_outputs = match state.generate_tx_outputs(outputs) { + let mut tx_outputs = match state.generate_tx_outputs(outputs, owned_utxo_notify_method) { Ok(u) => u, Err(err) => { tracing::error!("Could not generate tx outputs: {}", err); @@ -591,8 +664,8 @@ impl RPC for NeptuneRPCServer { let transaction = match state .create_transaction( &mut tx_outputs, - change_spending_key.into(), - UtxoNotifyMethod::OffChain, + change_key, + owned_utxo_notify_method, fee, now, ) @@ -606,21 +679,24 @@ impl RPC for NeptuneRPCServer { }; drop(state); - // Inform wallet of any expected incoming utxos. - // note that this (briefly) mutates self. - if let Err(e) = self - .state - .lock_guard_mut() - .await - .add_expected_utxos_to_wallet(tx_outputs.expected_utxos_iter()) - .await - { - tracing::error!("Could not add expected utxos to wallet: {}", e); - return None; - } + // if the tx created offchain expected_utxos we must inform wallet. + if tx_outputs.has_offchain() { + // acquire write-lock + let mut gsm = self.state.lock_guard_mut().await; + + // Inform wallet of any expected incoming utxos. + // note that this (briefly) mutates self. + if let Err(e) = gsm + .add_expected_utxos_to_wallet(tx_outputs.expected_utxos_iter()) + .await + { + tracing::error!("Could not add expected utxos to wallet: {}", e); + return None; + } - // ensure we write new state out to disk. - self.state.flush_databases().await.expect("flushed DBs"); + // ensure we write new wallet state out to disk. + gsm.persist_wallet().await.expect("flushed wallet"); + } // Send transaction message to main let response: Result<(), SendError> = self @@ -637,6 +713,7 @@ impl RPC for NeptuneRPCServer { } } + // documented in trait. do not add doc-comment. async fn shutdown(self, _: context::Context) -> bool { // 1. Send shutdown message to main let response = self @@ -648,6 +725,7 @@ impl RPC for NeptuneRPCServer { response.is_ok() } + // documented in trait. do not add doc-comment. async fn pause_miner(self, _context: tarpc::context::Context) { if self.state.cli().mine { let _ = self @@ -659,6 +737,7 @@ impl RPC for NeptuneRPCServer { } } + // documented in trait. do not add doc-comment. async fn restart_miner(self, _context: tarpc::context::Context) { if self.state.cli().mine { let _ = self @@ -670,6 +749,7 @@ impl RPC for NeptuneRPCServer { } } + // documented in trait. do not add doc-comment. async fn prune_abandoned_monitored_utxos(self, _context: tarpc::context::Context) -> usize { let mut global_state_mut = self.state.lock_guard_mut().await; const DEFAULT_MUTXO_PRUNE_DEPTH: usize = 200; @@ -695,7 +775,7 @@ impl RPC for NeptuneRPCServer { } } - #[doc = r" Generate a report of all owned and unspent coins, whether time-locked or not."] + // documented in trait. do not add doc-comment. async fn list_own_coins( self, _context: ::tarpc::context::Context, @@ -708,7 +788,7 @@ impl RPC for NeptuneRPCServer { .await } - #[doc = r" Return the temperature of the CPU in degrees Celcius."] + // documented in trait. do not add doc-comment. async fn cpu_temp(self, _context: tarpc::context::Context) -> Option { Self::cpu_temp_inner() } @@ -717,7 +797,7 @@ impl RPC for NeptuneRPCServer { #[cfg(test)] mod rpc_server_tests { use super::*; - use crate::models::state::wallet::address::generation_address::ReceivingAddress; + use crate::models::state::wallet::address::generation_address::GenerationReceivingAddress; use crate::models::state::wallet::utxo_notification_pool::{ExpectedUtxo, UtxoNotifier}; use crate::tests::shared::make_mock_block_with_valid_pow; use crate::Block; @@ -734,7 +814,7 @@ mod rpc_server_tests { use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use strum::IntoEnumIterator; use tracing_test::traced_test; - use ReceivingAddressType; + use ReceivingAddress; async fn test_rpc_server( network: Network, @@ -804,7 +884,10 @@ mod rpc_server_tests { let _ = rpc_server.clone().synced_balance(ctx).await; let _ = rpc_server.clone().history(ctx).await; let _ = rpc_server.clone().wallet_status(ctx).await; - let own_receiving_address = rpc_server.clone().next_receiving_address(ctx).await; + let own_receiving_address = rpc_server + .clone() + .next_receiving_address(ctx, KeyType::Generation) + .await; let _ = rpc_server.clone().mempool_tx_count(ctx).await; let _ = rpc_server.clone().mempool_size(ctx).await; let _ = rpc_server.clone().dashboard_overview_data(ctx).await; @@ -822,7 +905,8 @@ mod rpc_server_tests { .send( ctx, NeptuneCoins::one(), - own_receiving_address, + own_receiving_address.clone(), + UtxoNotifyMethod::OffChain, NeptuneCoins::one(), ) .await; @@ -831,6 +915,7 @@ mod rpc_server_tests { .send_to_many( ctx, vec![(own_receiving_address, NeptuneCoins::one())], + UtxoNotifyMethod::OffChain, NeptuneCoins::one(), ) .await; @@ -1309,16 +1394,16 @@ mod rpc_server_tests { // --- Init. get wallet spending key --- let genesis_block = Block::genesis_block(network); let wallet_spending_key = state_lock - .lock_guard() + .lock_guard_mut() .await .wallet_state - .get_known_generation_spending_keys()[0]; + .next_unused_spending_key(KeyType::Generation); // --- Init. generate a block, with coinbase going to our wallet --- let (block_1, cb_utxo, cb_output_randomness) = make_mock_block_with_valid_pow( &genesis_block, None, - wallet_spending_key.to_address(), + wallet_spending_key.to_address().try_into()?, rng.gen(), ); @@ -1331,7 +1416,7 @@ mod rpc_server_tests { ExpectedUtxo::new( cb_utxo, cb_output_randomness, - wallet_spending_key.privacy_preimage, + wallet_spending_key.privacy_preimage(), UtxoNotifier::OwnMiner, ), ) @@ -1339,21 +1424,18 @@ mod rpc_server_tests { // --- Setup. generate an output that our wallet cannot claim. --- let output1 = ( - ReceivingAddressType::from(ReceivingAddress::derive_from_seed(rng.gen())), + ReceivingAddress::from(GenerationReceivingAddress::derive_from_seed(rng.gen())), NeptuneCoins::new(5), ); // --- Setup. generate an output that our wallet can claim. --- let output2 = { let spending_key = state_lock - .lock_guard() + .lock_guard_mut() .await .wallet_state - .get_known_generation_spending_keys()[0]; - ( - ReceivingAddressType::from(spending_key.to_address()), - NeptuneCoins::new(25), - ) + .next_unused_spending_key(KeyType::Generation); + (spending_key.to_address(), NeptuneCoins::new(25)) }; // --- Setup. assemble outputs and fee --- @@ -1369,7 +1451,10 @@ mod rpc_server_tests { .len(); // --- Operation: perform send_to_many - let result = rpc_server.clone().send_to_many(ctx, outputs, fee).await; + let result = rpc_server + .clone() + .send_to_many(ctx, outputs, UtxoNotifyMethod::OffChain, fee) + .await; // --- Test: verify op returns a value. assert!(result.is_some()); diff --git a/src/tests/shared.rs b/src/tests/shared.rs index bea6c35b..7d8b12dd 100644 --- a/src/tests/shared.rs +++ b/src/tests/shared.rs @@ -795,7 +795,7 @@ pub fn make_mock_block( previous_block: &Block, // target_difficulty: Option>, block_timestamp: Option, - coinbase_beneficiary: generation_address::ReceivingAddress, + coinbase_beneficiary: generation_address::GenerationReceivingAddress, seed: [u8; 32], ) -> (Block, Utxo, Digest) { let mut rng: StdRng = SeedableRng::from_seed(seed); @@ -886,7 +886,7 @@ pub fn make_mock_block( pub fn make_mock_block_with_valid_pow( previous_block: &Block, block_timestamp: Option, - coinbase_beneficiary: generation_address::ReceivingAddress, + coinbase_beneficiary: generation_address::GenerationReceivingAddress, seed: [u8; 32], ) -> (Block, Utxo, Digest) { let mut rng: StdRng = SeedableRng::from_seed(seed); @@ -913,7 +913,7 @@ pub fn make_mock_block_with_valid_pow( pub fn make_mock_block_with_invalid_pow( previous_block: &Block, block_timestamp: Option, - coinbase_beneficiary: generation_address::ReceivingAddress, + coinbase_beneficiary: generation_address::GenerationReceivingAddress, seed: [u8; 32], ) -> (Block, Utxo, Digest) { let mut rng: StdRng = SeedableRng::from_seed(seed); diff --git a/src/util_types/mutator_set/shared.rs b/src/util_types/mutator_set/shared.rs index 9305a57e..be198524 100644 --- a/src/util_types/mutator_set/shared.rs +++ b/src/util_types/mutator_set/shared.rs @@ -41,10 +41,10 @@ pub fn indices_to_hash_map(all_indices: &[u128; NUM_TRIALS as usize]) -> HashMap /// /// Returns: /// - 0: A hash set of indices, showing which indices are into the chunk dictionaries -/// which have modified chunks. +/// which have modified chunks. /// - 1: A list of (old membership proof, new digest) where the membership proof -/// is how it looks before applying the removal record, and the digest is how -/// it looks after applying the removal record. +/// is how it looks before applying the removal record, and the digest is how +/// it looks after applying the removal record. /// /// This function updates the chunks that are present in the `chunk_dictionaries` /// input argument, but not the associated membership proofs. That must be handled @@ -139,10 +139,10 @@ pub fn get_batch_mutation_argument_for_removal_record( /// /// Returns: /// - 0: A hash set of indices, showing which indices are into the chunk dictionaries -/// which have modified chunks. +/// which have modified chunks. /// - 1: A list of (old membership proof, new digest) where the membership proof -/// is how it looks before applying the removal record, and the digest is how -/// it looks after applying the removal record. +/// is how it looks before applying the removal record, and the digest is how +/// it looks after applying the removal record. /// /// This function updates the chunks that are present in the `chunk_dictionaries` /// input argument.