diff --git a/Cargo.toml b/Cargo.toml index 64020f17..f2a501e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ bincode = "1.3" bytes = "1.6" bytesize = "1.3" chrono = "=0.4.34" -clap = { version = "4.5", features = ["derive"] } +clap = { version = "4.5", features = ["derive", "string"] } clap_complete = "4.4" console-subscriber = "0.2" crossterm = "0.27" diff --git a/src/bin/dashboard_src/send_screen.rs b/src/bin/dashboard_src/send_screen.rs index 87d5038f..2e2f8997 100644 --- a/src/bin/dashboard_src/send_screen.rs +++ b/src/bin/dashboard_src/send_screen.rs @@ -8,7 +8,8 @@ use crossterm::event::Event; use crossterm::event::KeyCode; use crossterm::event::KeyEventKind; use neptune_core::config_models::network::Network; -use neptune_core::models::blockchain::transaction::UtxoNotifyMethod; +use neptune_core::models::blockchain::transaction::OwnedUtxoNotifyMethod; +use neptune_core::models::blockchain::transaction::UnownedUtxoNotifyMethod; use neptune_core::models::blockchain::type_scripts::neptune_coins::NeptuneCoins; use neptune_core::models::state::wallet::address::ReceivingAddress; use neptune_core::rpc_server::RPCClient; @@ -136,18 +137,23 @@ impl SendScreen { let mut send_ctx = context::current(); 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( + + // todo: make owned/unowned notify method configurable. + let (tx_params, _outputs_map) = rpc_client + .generate_tx_params( send_ctx, - valid_amount, - valid_address, - UtxoNotifyMethod::OnChain, + vec![(valid_address, valid_amount)], fee, + OwnedUtxoNotifyMethod::default(), + UnownedUtxoNotifyMethod::default(), ) .await + .unwrap() .unwrap(); - if send_result.is_none() { + let send_result = rpc_client.send(send_ctx, tx_params).await.unwrap(); + + if send_result.is_err() { *notice_arc.lock().await = "Could not send due to error.".to_string(); *focus_arc.lock().await = SendScreenWidget::Address; return; diff --git a/src/bin/neptune-cli.rs b/src/bin/neptune-cli.rs index b84dea9a..932f54b1 100644 --- a/src/bin/neptune-cli.rs +++ b/src/bin/neptune-cli.rs @@ -1,36 +1,52 @@ use std::io; use std::io::stdout; +use std::io::Read; use std::io::Write; use std::net::IpAddr; use std::net::SocketAddr; +use std::path::PathBuf; use std::str::FromStr; +use anyhow::anyhow; use anyhow::bail; use anyhow::Result; use clap::CommandFactory; use clap::Parser; +use clap::Subcommand; use clap_complete::generate; use clap_complete::Shell; +use itertools::EitherOrBoth; +use itertools::Itertools; 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::transaction::OwnedUtxoNotifyMethod; +use neptune_core::models::blockchain::transaction::TxParams; +use neptune_core::models::blockchain::transaction::UnownedUtxoNotifyMethod; +use neptune_core::models::blockchain::transaction::UtxoNotification; use neptune_core::models::blockchain::type_scripts::neptune_coins::NeptuneCoins; 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; +use neptune_core::models::state::TxOutputMeta; use neptune_core::rpc_server::RPCClient; +use serde::Deserialize; +use serde::Serialize; use tarpc::client; use tarpc::context; use tarpc::tokio_serde::formats::Json; -// for parsing SendToMany arguments. +const SELF: &str = "self"; +const ANONYMOUS: &str = "anonymous"; + +/// for parsing SendToMany `` arguments. #[derive(Debug, Clone)] struct TransactionOutput { address: String, amount: NeptuneCoins, + recipient: String, } /// We impl FromStr deserialization so that clap can parse the --outputs arg of @@ -41,28 +57,36 @@ struct TransactionOutput { impl FromStr for TransactionOutput { type Err = anyhow::Error; - /// parses address:amount into TransactionOutput{address, amount} + /// parses address:amount:recipient or address:amount into TransactionOutput{address, amount, recipient} + /// + /// This is used by the outputs arg of send-to-many command. Usage looks + /// like: /// - /// This is used by the outputs arg of send-to-many command. - /// Usage looks like: + /// ... format: address:amount address:amount:recipient ... /// - /// ... format: address:amount address:amount ... + /// So each output is space delimited and the 2 (or 3) fields are colon + /// delimited. /// - /// So each output is space delimited and the two fields are - /// colon delimted. + /// if `recipient` is omitted for any output, then it defaults to + /// "anonymous". /// - /// This format was chosen because it should be simple for humans - /// to generate on the command-line. + /// This format was chosen because it should be simple for humans to + /// generate on the command-line. fn from_str(s: &str) -> Result { let parts = s.split(':').collect::>(); - if parts.len() != 2 { + if parts.len() != 2 && parts.len() != 3 { anyhow::bail!("Invalid transaction output. missing :") } Ok(Self { address: parts[0].to_string(), amount: NeptuneCoins::from_str(parts[1])?, + recipient: match parts.len() { + 3 if !parts[2].trim().is_empty() => parts[2].trim(), + _ => ANONYMOUS, + } + .to_string(), }) } } @@ -79,80 +103,274 @@ impl TransactionOutput { } } -#[derive(Debug, Parser)] +/// AddressEnum is used by send and send-to-many to distinguish between +/// key-types when writing utxo-transfer file(s) for any off-chain-serialized +/// utxos. +/// +/// the issue is that it is useful to display the address in the file, or even an +/// abbreviation in the filename. This aids the sender in identifying the utxo +/// and routing it to the intended recipient. +/// +/// however this should never be done for symmetric keys as it would expose the +/// private key, so we only display the receiver_identifier. +/// +/// normally unowned utxo-transfer would not be using symmetric keys, however +/// there are some use cases for it such as when a person or org holds multiple +/// wallets and is transferrng between them. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AddressEnum { + Generation { + address_abbrev: String, + address: String, + receiver_identifier: String, + }, + Symmetric { + receiver_identifier: String, + }, +} +impl TryFrom<(&ReceivingAddress, Network)> for AddressEnum { + type Error = anyhow::Error; + + fn try_from(v: (&ReceivingAddress, Network)) -> Result { + let (addr, network) = v; + Ok(match *addr { + ReceivingAddress::Generation(_) => Self::Generation { + address_abbrev: addr.to_bech32m_abbreviated(network)?, + address: addr.to_bech32m(network)?, + receiver_identifier: addr.receiver_identifier().to_string(), + }, + ReceivingAddress::Symmetric(_) => Self::Symmetric { + receiver_identifier: addr.receiver_identifier().to_string(), + }, + }) + } +} +impl AddressEnum { + fn short_id(&self) -> &str { + match *self { + Self::Generation { + ref address_abbrev, .. + } => address_abbrev, + Self::Symmetric { + ref receiver_identifier, + .. + } => receiver_identifier, + } + } +} + +/// represents a UtxoTransfer entry in a utxo-transfer file. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UtxoTransferEntry { + pub data_format: String, + pub recipient: String, + pub amount: String, + pub utxo_transfer_encrypted: String, + pub address_info: AddressEnum, +} + +impl UtxoTransferEntry { + fn data_format() -> String { + "neptune-utxo-transfer-v1.0".to_string() + } +} + +/// represents data format of input to claim-utxo +#[derive(Debug, Clone, Subcommand)] +pub enum ClaimUtxoFormat { + /// reads utxo-transfer-encrypted field of the utxo-transfer json file. + Raw { + /// will be read from stdin if not present + raw: Option, + }, + /// reads contents of a utxo-transfer json file + Json { + /// will be read from stdin if not present + json: Option, + }, + /// reads a utxo-transfer json file + File { + /// path to the file + path: PathBuf, + }, +} + +/// represents cli command +#[derive(Debug, Clone, Parser)] enum Command { /// Dump shell completions. Completions, /******** READ STATE ********/ + /// retrieve network that neptune-core is running on Network, + + /// retrieve address for peers to contact this neptune-core node OwnListenAddressForPeers, + + /// retrieve instance-id of this neptune-core node OwnInstanceId, + + /// retrieve current block height BlockHeight, + + /// retrieve information about a block BlockInfo { /// one of: `genesis, tip, height/, digest/` block_selector: BlockSelector, }, + + /// retrieve confirmations Confirmations, + + /// retrieve info about peers PeerInfo, + + /// retrieve list of sanctioned peers AllSanctionedPeers, + + /// retrieve digest/hash of newest block TipDigest, - LatestTipDigests { - n: usize, - }, - TipHeader, + + /// retrieve digests of newest n blocks + LatestTipDigests { n: usize }, + + /// retrieve block-header of any block Header { /// one of: `genesis, tip, height/, digest/` block_selector: BlockSelector, }, + + /// retrieve confirmed balance SyncedBalance, + + /// retrieve wallet status information WalletStatus, - OwnReceivingAddress, + + /// retrieve next unused receiving address + NextReceivingAddress { + #[clap(value_enum, default_value_t = KeyType::Generation)] + key_type: KeyType, + }, + + /// list known coins ListCoins, + + /// retrieve count of transactions in the mempool MempoolTxCount, + + /// retrieve size of mempool in bytes (in RAM) MempoolSize, /******** CHANGE STATE ********/ + /// shutdown neptune-core Shutdown, + + /// clear all peer standings ClearAllStandings, - ClearStandingByIp { - ip: IpAddr, - }, + + /// clear peer standing for a given IP address + ClearStandingByIp { ip: IpAddr }, + + /// send to a single recipient Send { - amount: NeptuneCoins, + /// recipient's address address: String, + + /// amount to send + amount: NeptuneCoins, + + /// transaction fee fee: NeptuneCoins, + + /// recipient name or label, for local usage only + #[clap(value_parser = clap::builder::NonEmptyStringValueParser::new(), default_value = ANONYMOUS)] + recipient: String, + + /// how to notify our wallet of utxos. + #[clap(long, value_enum, default_value_t)] + owned_utxo_notify_method: OwnedUtxoNotifyMethod, + + /// how to notify recipient's wallet of utxos. + #[clap(long, value_enum, default_value_t)] + unowned_utxo_notify_method: UnownedUtxoNotifyMethod, }, + + /// send to multiple recipients SendToMany { - /// format: address:amount address:amount ... + /// transaction outputs. format: address:amount:recipient address:amount ... + /// + /// recipient is a local-only label and will be "anonymous" if omitted. #[clap(value_parser, num_args = 1.., required=true, value_delimiter = ' ')] outputs: Vec, + + /// transaction fee fee: NeptuneCoins, + + /// how to notify our wallet of utxos. + #[clap(long, value_enum, default_value_t)] + owned_utxo_notify_method: OwnedUtxoNotifyMethod, + + /// how to notify recipient's wallet(s) of utxos. + #[clap(long, value_enum, default_value_t)] + unowned_utxo_notify_method: UnownedUtxoNotifyMethod, + }, + /// claim an off-chain utxo-transfer. + ClaimUtxo { + #[clap(subcommand)] + format: ClaimUtxoFormat, }, + + /// pause mining PauseMiner, + + /// resume mining RestartMiner, + + /// prune monitored utxos from abandoned chains PruneAbandonedMonitoredUtxos, /******** WALLET ********/ + /// generate a new wallet GenerateWallet { - #[clap(long, default_value_t=Network::default())] + #[clap(long, default_value_t)] network: Network, + + /// neptune-core data directory containing wallet and blockchain state + #[clap(long)] + data_dir: Option, }, + /// displays path to wallet secrets file WhichWallet { - #[clap(long, default_value_t=Network::default())] + #[clap(long, default_value_t)] network: Network, + + /// neptune-core data directory containing wallet and blockchain state + #[clap(long)] + data_dir: Option, }, + /// export mnemonic seed phrase ExportSeedPhrase { - #[clap(long, default_value_t=Network::default())] + #[clap(long, default_value_t)] network: Network, + + /// neptune-core data directory containing wallet and blockchain state + #[clap(long)] + data_dir: Option, }, + /// import mnemonic seed phrase ImportSeedPhrase { - #[clap(long, default_value_t=Network::default())] + #[clap(long, default_value_t)] network: Network, + + /// neptune-core data directory containing wallet and blockchain state + #[clap(long)] + data_dir: Option, }, } -#[derive(Debug, Parser)] +/// represents top-level cli args +#[derive(Debug, Clone, Parser)] #[clap(name = "neptune-cli", about = "An RPC client")] struct Config { /// Sets the server address to connect to. @@ -161,9 +379,6 @@ struct Config { #[clap(subcommand)] command: Command, - - #[structopt(long, short, default_value = "alpha")] - pub network: Network, } #[tokio::main] @@ -180,30 +395,25 @@ async fn main() -> Result<()> { bail!("Unknown shell. Shell completions not available.") } } - Command::WhichWallet { network } => { - // The root path is where both the wallet and all databases are stored - let data_dir = DataDirectory::get(None, network)?; + Command::WhichWallet { network, data_dir } => { + let wallet_dir = DataDirectory::get(data_dir, network)?.wallet_directory_path(); // Get wallet object, create various wallet secret files - let wallet_dir = data_dir.wallet_directory_path(); let wallet_file = WalletSecret::wallet_secret_path(&wallet_dir); if !wallet_file.exists() { - println!("No wallet file found at {}.", wallet_file.display()); + bail!("No wallet file found at {}.", wallet_file.display()); } else { println!("{}", wallet_file.display()); } return Ok(()); } - Command::GenerateWallet { network } => { - // The root path is where both the wallet and all databases are stored - let data_dir = DataDirectory::get(None, network)?; + Command::GenerateWallet { network, data_dir } => { + let wallet_dir = DataDirectory::get(data_dir, network)?.wallet_directory_path(); // Get wallet object, create various wallet secret files - let wallet_dir = data_dir.wallet_directory_path(); DataDirectory::create_dir_if_not_exists(&wallet_dir).await?; - let (_, secret_file_paths) = - WalletSecret::read_from_file_or_create(&wallet_dir).unwrap(); + let (_, secret_file_paths) = WalletSecret::read_from_file_or_create(&wallet_dir)?; println!( "Wallet stored in: {}\nMake sure you also see this path if you run the neptune-core client", @@ -217,19 +427,16 @@ async fn main() -> Result<()> { return Ok(()); } - Command::ImportSeedPhrase { network } => { - // The root path is where both the wallet and all databases are stored - let data_dir = DataDirectory::get(None, network)?; - let wallet_dir = data_dir.wallet_directory_path(); + Command::ImportSeedPhrase { network, data_dir } => { + let wallet_dir = DataDirectory::get(data_dir, network)?.wallet_directory_path(); let wallet_file = WalletSecret::wallet_secret_path(&wallet_dir); // if the wallet file already exists, if wallet_file.exists() { - println!( + bail!( "Cannot import seed phrase; wallet file {} already exists. Move it to another location (or remove it) to import a seed phrase.", wallet_file.display() ); - return Ok(()); } // read seed phrase from user input @@ -261,8 +468,7 @@ async fn main() -> Result<()> { } let wallet_secret = match WalletSecret::from_phrase(&phrase) { Err(_) => { - println!("Invalid seed phrase. Please try again."); - return Ok(()); + bail!("Invalid seed phrase."); } Ok(ws) => ws, }; @@ -272,9 +478,7 @@ async fn main() -> Result<()> { DataDirectory::create_dir_if_not_exists(&wallet_dir).await?; match wallet_secret.save_to_disk(&wallet_file) { Err(e) => { - println!("Could not save imported wallet to disk."); - println!("Error:"); - println!("{e}"); + bail!("Could not save imported wallet to disk. {e}"); } Ok(_) => { println!("Success."); @@ -283,19 +487,17 @@ async fn main() -> Result<()> { return Ok(()); } - Command::ExportSeedPhrase { network } => { + Command::ExportSeedPhrase { network, data_dir } => { // The root path is where both the wallet and all databases are stored - let data_dir = DataDirectory::get(None, network)?; + let wallet_dir = DataDirectory::get(data_dir, network)?.wallet_directory_path(); // Get wallet object, create various wallet secret files - let wallet_dir = data_dir.wallet_directory_path(); let wallet_file = WalletSecret::wallet_secret_path(&wallet_dir); if !wallet_file.exists() { - println!( - "Cannot export seed phrase because there is no wallet.dat file to export from." + bail!( + concat!("Cannot export seed phrase because there is no wallet.dat file to export from.\n", + "Generate one using `neptune-cli generate-wallet` or `neptune-wallet-gen`, or import a seed phrase using `neptune-cli import-seed-phrase`.") ); - println!("Generate one using `neptune-cli generate-wallet` or `neptune-wallet-gen`, or import a seed phrase using `neptune-cli import-seed-phrase`."); - return Ok(()); } let wallet_secret = match WalletSecret::read_from_file(&wallet_file) { Err(e) => { @@ -319,7 +521,7 @@ async fn main() -> Result<()> { let client = RPCClient::new(client::Config::default(), transport.await?).spawn(); let ctx = context::current(); - match args.command { + match args.clone().command { Command::Completions | Command::GenerateWallet { .. } | Command::WhichWallet { .. } @@ -396,21 +598,10 @@ async fn main() -> Result<()> { println!("{hash}"); } } - Command::TipHeader => { - let val = client - .header(ctx, BlockSelector::Tip) - .await? - .expect("Tip header should be found"); - println!("{val}") - } - Command::Header { block_selector } => { - let res = client.header(ctx, block_selector).await?; - if res.is_none() { - println!("Block did not exist in database."); - } else { - println!("{}", res.unwrap()); - } - } + Command::Header { block_selector } => match client.header(ctx, block_selector).await? { + Some(header) => println!("{}", header), + None => println!("Block did not exist in database."), + }, Command::SyncedBalance => { let val = client.synced_balance(ctx).await?; println!("{val}"); @@ -419,11 +610,9 @@ async fn main() -> Result<()> { let wallet_status: WalletStatus = client.wallet_status(ctx).await?; println!("{}", serde_json::to_string_pretty(&wallet_status)?); } - Command::OwnReceivingAddress => { - let rec_addr = client - .next_receiving_address(ctx, KeyType::Generation) - .await?; - println!("{}", rec_addr.to_bech32m(args.network).unwrap()) + Command::NextReceivingAddress { key_type } => { + let rec_addr = client.next_receiving_address(ctx, key_type).await?; + println!("{}", rec_addr.to_bech32m(client.network(ctx).await?)?) } Command::MempoolTxCount => { let count: usize = client.mempool_tx_count(ctx).await?; @@ -449,34 +638,92 @@ async fn main() -> Result<()> { println!("Cleared standing of {}", ip); } Command::Send { - amount, address, + amount, fee, + recipient, + owned_utxo_notify_method, + unowned_utxo_notify_method, } => { // Parse on client - let receiving_address = ReceivingAddress::from_bech32m(&address, args.network)?; + let data_dir = client.data_directory(ctx).await?; + let network = client.network(ctx).await?; + let receiving_address = ReceivingAddress::from_bech32m(&address, network)?; + let parsed_outputs = vec![(receiving_address, amount)]; + let recipients = vec![recipient]; - client - .send( + let (tx_params, tx_output_meta) = client + .generate_tx_params( ctx, - amount, - receiving_address, - UtxoNotifyMethod::OnChain, + parsed_outputs.clone(), fee, + owned_utxo_notify_method, + unowned_utxo_notify_method, ) - .await?; - println!("Send completed."); + .await? + .map_err(|s| anyhow!(s))?; + + let tx_digest = client + .send(ctx, tx_params.clone()) + .await? + .map_err(|s| anyhow!(s))?; + + process_utxo_notifications(&data_dir, network, tx_params, tx_output_meta, recipients)?; + println!("Send completed. Tx Digest: {}", tx_digest.to_hex()); } - Command::SendToMany { outputs, fee } => { + Command::SendToMany { + outputs, + fee, + owned_utxo_notify_method, + unowned_utxo_notify_method, + } => { + let data_dir = client.data_directory(ctx).await?; + let network = client.network(ctx).await?; let parsed_outputs = outputs - .into_iter() - .map(|o| o.to_receiving_address_amount_tuple(args.network)) + .iter() + .map(|o| o.to_receiving_address_amount_tuple(network)) .collect::>>()?; + let (tx_params, tx_output_meta) = client + .generate_tx_params( + ctx, + parsed_outputs.clone(), + fee, + owned_utxo_notify_method, + unowned_utxo_notify_method, + ) + .await? + .map_err(|s| anyhow!(s))?; + + let tx_digest = client + .send(ctx, tx_params.clone()) + .await? + .map_err(|s| anyhow!(s))?; + + let recipients = outputs.into_iter().map(|o| o.recipient).collect_vec(); + process_utxo_notifications(&data_dir, network, tx_params, tx_output_meta, recipients)?; + println!("Send completed. Tx Digest: {}", tx_digest.to_hex()); + } + Command::ClaimUtxo { format } => { + let utxo_transfer_encrypted = match format { + ClaimUtxoFormat::Raw { raw } => val_or_stdin_line(raw)?, + ClaimUtxoFormat::File { path } => { + let buf = std::fs::read_to_string(path)?; + let utxo_transfer_entry: UtxoTransferEntry = serde_json::from_str(&buf)?; + utxo_transfer_entry.utxo_transfer_encrypted + } + ClaimUtxoFormat::Json { json } => { + let buf = val_or_stdin(json)?; + let utxo_transfer_entry: UtxoTransferEntry = serde_json::from_str(&buf)?; + utxo_transfer_entry.utxo_transfer_encrypted + } + }; client - .send_to_many(ctx, parsed_outputs, UtxoNotifyMethod::OnChain, fee) - .await?; - println!("Send completed."); + .claim_utxo(ctx, utxo_transfer_encrypted) + .await? + .map_err(|s| anyhow!(s))?; + + println!("Success. 1 Utxo Transfer was imported."); } Command::PauseMiner => { println!("Sending command to pause miner."); @@ -497,3 +744,139 @@ async fn main() -> Result<()> { Ok(()) } + +// processes utxo-notifications in TxParams outputs, if any. +// +// 1. find off-chain-serialized outputs and add metadata +// (address, label, owner-type) +// 2. create utxo-transfer dir if not existing +// 3. write out one UtxoTransferEntry in a json file, per output +// 4. provide instructions for sender and receiver. (if needed) +fn process_utxo_notifications( + root_data_dir: &DataDirectory, + network: Network, + tx_params: TxParams, + tx_output_meta: Vec, + recipients: Vec, +) -> anyhow::Result<()> { + assert_eq!(tx_params.tx_output_list().len(), tx_output_meta.len()); + assert!(tx_output_meta.len() >= recipients.len()); + + let data_dir = root_data_dir.utxo_transfer_directory_path(); + + // find off-chain-serialized outputs and add metadata + // (address, label, owner-type) + let mut entries = tx_params + .tx_output_list() + .iter() + .zip(tx_output_meta) + .zip_longest(recipients) + .map(|pair| match pair { + EitherOrBoth::Both((o, m), r) => (o, m, r), + EitherOrBoth::Left((o, m)) => (o, m, SELF.to_string()), + EitherOrBoth::Right(_) => unreachable!(), + }) + .filter_map(|(o, m, recipient)| match &o.utxo_notification { + UtxoNotification::OffChainSerialized(x) => Some(( + m.receiving_address, + o.utxo.get_native_currency_amount(), + x, + match m.self_owned { + true => SELF.to_string(), + false => recipient, + }, + )), + _ => None, + }) + .peekable(); + + if entries.peek().is_some() { + // create utxo-transfer dir if not existing + std::fs::create_dir_all(&data_dir)?; + + println!("\n*** Utxo Transfer Files ***\n"); + } + + // write out one UtxoTransferEntry in a json file, per output + let mut wrote_file_cnt = 0usize; + for (address, amount, utxo_transfer_encrypted, recipient) in entries { + let file_dir = data_dir.join(&recipient); + std::fs::create_dir_all(&file_dir)?; + + let entry = UtxoTransferEntry { + data_format: UtxoTransferEntry::data_format(), + recipient: recipient.clone(), + amount: amount.to_string(), + utxo_transfer_encrypted: utxo_transfer_encrypted.to_bech32m(network)?, + address_info: (&address, network).try_into()?, + }; + + let mut file_name = format!("{}-{}.json", entry.address_info.short_id(), entry.amount); + let file_path = (1..) + .filter_map(|i| { + let path = file_dir.join(&file_name); + file_name = format!("{}-{}.{}.json", recipient, entry.amount, i); + match path.exists() { + false => Some(path), + true => None, + } + }) + .next() + .ok_or_else(|| anyhow!("could not determine file path"))?; + + let file = std::fs::File::create_new(&file_path)?; + let mut writer = std::io::BufWriter::new(file); + serde_json::to_writer_pretty(&mut writer, &entry)?; + writer.flush()?; + + wrote_file_cnt += 1; + + println!("wrote {}", file_path.display()); + } + + // provide instructions for sender and receiver. (if needed) + if wrote_file_cnt > 0 { + println!("\n*** Important - Read or risk losing funds ***\n"); + println!( + " +{wrote_file_cnt} transaction outputs were each written to individual files for off-chain transfer. + +-- Sender Instructions -- + +You must transfer each file to the corresponding recipient for claiming or they will never be able to claim the funds. + +You should also provide them the following recipient instructions. + +-- Recipient Instructions -- + +run `neptune-cli claim-utxo file ` or use equivalent claim functionality of your chosen wallet software. +" + ); + } + + Ok(()) +} + +// retrieves value from Option or else from first line of stdin (trimmed). +fn val_or_stdin_line(val: Option) -> Result { + match val { + Some(v) => Ok(v.to_string()), + None => { + let mut buffer = String::new(); + std::io::stdin().read_line(&mut buffer)?; + Ok(buffer.trim().to_string()) + } + } +} + +// retrieves value from Option or else from stdin (trimmed). +fn val_or_stdin(val: Option) -> Result { + match val { + Some(v) => Ok(v.to_string()), + None => { + let mut buffer = String::new(); + std::io::stdin().read_to_string(&mut buffer)?; + Ok(buffer.trim().to_string()) + } + } +} diff --git a/src/config_models/data_directory.rs b/src/config_models/data_directory.rs index bbe16abe..33cb93c7 100644 --- a/src/config_models/data_directory.rs +++ b/src/config_models/data_directory.rs @@ -4,6 +4,8 @@ use std::path::PathBuf; use anyhow::Context; use anyhow::Result; use directories::ProjectDirs; +use serde::Deserialize; +use serde::Serialize; use crate::config_models::network::Network; use crate::models::database::DATABASE_DIRECTORY_ROOT_NAME; @@ -17,8 +19,10 @@ use crate::models::state::wallet::WALLET_DB_NAME; use crate::models::state::wallet::WALLET_DIRECTORY; use crate::models::state::wallet::WALLET_OUTPUT_COUNT_DB_NAME; +const UTXO_TRANSFER_DIRECTORY: &str = "utxo-transfer"; + // TODO: Add `rusty_leveldb::Options` and `fs::OpenOptions` here too, since they keep being repeated. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct DataDirectory { data_dir: PathBuf, } @@ -91,6 +95,18 @@ impl DataDirectory { self.database_dir_path().join(Path::new(BANNED_IPS_DB_NAME)) } + /////////////////////////////////////////////////////////////////////////// + /// + /// utxo-transfer path + /// + /// for storing off-chain serialized transfer files. + /// + /// note: this is not used by neptune-core, but is used/shared by + /// neptune-cli, neptune-dashboard + pub fn utxo_transfer_directory_path(&self) -> PathBuf { + self.data_dir.join(Path::new(UTXO_TRANSFER_DIRECTORY)) + } + /////////////////////////////////////////////////////////////////////////// /// /// The wallet file path diff --git a/src/config_models/network.rs b/src/config_models/network.rs index 5b706352..9f250ee2 100644 --- a/src/config_models/network.rs +++ b/src/config_models/network.rs @@ -5,35 +5,36 @@ use std::time::UNIX_EPOCH; use serde::Deserialize; use serde::Serialize; -use strum::EnumIter; use tasm_lib::twenty_first::math::b_field_element::BFieldElement; use crate::models::consensus::timestamp::Timestamp; -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Default, EnumIter)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Default, clap::ValueEnum)] pub enum Network { /// First iteration of testnet. Not feature-complete. Soon to be deprecated. #[default] Alpha, - /// Upcoming iteration of testnet. Not feature-complete either but moreso than - /// Alpha. Soon to be set as default. + /// Upcoming iteration of testnet. + /// + /// Not feature-complete either but moreso than Alpha. Soon to be set as default. Beta, /// Main net. Feature-complete. Fixed launch date. Not ready yet. Main, - /// Feature-complete (or as feature-complete as possible) test network separate - /// from whichever network is currently running. For integration tests involving - /// multiple nodes over a network. + /// Feature-complete test network (eventually). + /// + /// For integration tests involving multiple nodes over a network. Testnet, - /// Network for individual unit and integration tests. The timestamp for the - /// RegTest genesis block is set to now, rounded down to the first block of - /// 10 hours. As a result, there is a small probability that tests fail - /// because they generate the genesis block twice on two opposite sides of a - /// round timestamp. - RegTest, + /// Network for development and tests. + /// + /// The timestamp for the RegTest genesis block is set to now, rounded down + /// to the first block of 10 hours. As a result, there is a small + /// probability that tests fail because they generate the genesis block + /// twice on two opposite sides of a round timestamp. + Regtest, } impl Network { pub(crate) fn launch_date(&self) -> Timestamp { @@ -44,7 +45,7 @@ impl Network { const TEN_HOURS_AS_MS: u64 = 1000 * 60 * 60 * 10; let now_rounded = (now / TEN_HOURS_AS_MS) * TEN_HOURS_AS_MS; match self { - Network::RegTest => Timestamp(BFieldElement::new(now_rounded)), + Network::Regtest => Timestamp(BFieldElement::new(now_rounded)), // 1 July 2024 (might be revised though) Network::Alpha | Network::Testnet | Network::Beta | Network::Main => { Timestamp(BFieldElement::new(1719792000000u64)) @@ -58,7 +59,7 @@ impl fmt::Display for Network { let string = match self { Network::Alpha => "alpha".to_string(), Network::Testnet => "testnet".to_string(), - Network::RegTest => "regtest".to_string(), + Network::Regtest => "regtest".to_string(), Network::Beta => "beta".to_string(), Network::Main => "main".to_string(), }; @@ -72,7 +73,7 @@ impl FromStr for Network { match input { "alpha" => Ok(Network::Alpha), "testnet" => Ok(Network::Testnet), - "regtest" => Ok(Network::RegTest), + "regtest" => Ok(Network::Regtest), "beta" => Ok(Network::Beta), "main" => Ok(Network::Main), _ => Err(format!("Failed to parse {} as network", input)), diff --git a/src/lib.rs b/src/lib.rs index 64f43b93..2c0b3a88 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,9 +66,7 @@ use crate::models::channel::PeerTaskToMain; use crate::models::channel::RPCServerToMain; use crate::models::peer::HandshakeData; use crate::models::state::archival_state::ArchivalState; -use crate::models::state::blockchain_state::BlockchainArchivalState; use crate::models::state::blockchain_state::BlockchainState; -use crate::models::state::light_state::LightState; use crate::models::state::mempool::Mempool; use crate::models::state::networking_state::NetworkingState; use crate::models::state::wallet::wallet_state::WalletState; @@ -119,7 +117,7 @@ pub async fn initialize(cli_args: cli_args::Args) -> Result<()> { .await; // Get latest block. Use hardcoded genesis block if nothing is in database. - let latest_block: Block = archival_state.get_tip().await; + let latest_block = archival_state.tip().to_owned(); // Bind socket to port on this machine, to handle incoming connections from peers let incoming_peer_listener = TcpListener::bind((cli_args.listen_addr, cli_args.peer_port)) @@ -142,12 +140,7 @@ pub async fn initialize(cli_args: cli_args::Args) -> Result<()> { let syncing = false; let networking_state = NetworkingState::new(peer_map, peer_databases, syncing); - let light_state: LightState = LightState::from(latest_block.clone()); - let blockchain_archival_state = BlockchainArchivalState { - light_state, - archival_state, - }; - let blockchain_state = BlockchainState::Archival(blockchain_archival_state); + let blockchain_state = BlockchainState::Archival(archival_state); let mempool = Mempool::new(cli_args.max_mempool_size, latest_block.hash()); let mut global_state_lock = GlobalStateLock::new( wallet_state, diff --git a/src/main_loop.rs b/src/main_loop.rs index 050c9e99..8451c758 100644 --- a/src/main_loop.rs +++ b/src/main_loop.rs @@ -1024,11 +1024,6 @@ impl MainLoopHandler { self.main_to_peer_broadcast_tx .send(MainToPeerTask::TransactionNotification(notification))?; - // insert transaction into mempool - self.global_state_lock - .lock_mut(|s| s.mempool.insert(&transaction)) - .await; - // do not shut down Ok(false) } diff --git a/src/mine_loop.rs b/src/mine_loop.rs index e37f2acc..8fb84a78 100644 --- a/src/mine_loop.rs +++ b/src/mine_loop.rs @@ -51,7 +51,7 @@ use crate::util_types::mutator_set::mutator_set_accumulator::MutatorSetAccumulat const MOCK_MAX_BLOCK_SIZE: u32 = 1_000_000; /// Prepare a Block for mining -fn make_block_template( +pub(crate) fn make_block_template( previous_block: &Block, transaction: Transaction, mut block_timestamp: Timestamp, @@ -299,7 +299,7 @@ fn make_coinbase_transaction( /// Create the transaction that goes into the block template. The transaction is /// built from the mempool and from the coinbase transaction. Also returns the /// "sender randomness" used in the coinbase transaction. -fn create_block_transaction( +pub(crate) fn create_block_transaction( latest_block: &Block, global_state: &GlobalState, timestamp: Timestamp, @@ -540,7 +540,7 @@ mod mine_loop_tests { #[tokio::test] async fn block_template_is_valid_test() -> Result<()> { // Verify that a block template made with transaction from the mempool is a valid block - let network = Network::RegTest; + let network = Network::Regtest; let mut premine_receiver_global_state_lock = mock_genesis_global_state(network, 2, WalletSecret::devnet_wallet()).await; let mut premine_receiver_global_state = @@ -663,7 +663,7 @@ mod mine_loop_tests { #[traced_test] #[tokio::test] async fn mined_block_has_proof_of_work() -> Result<()> { - let network = Network::RegTest; + let network = Network::Regtest; let global_state_lock = mock_genesis_global_state(network, 2, WalletSecret::devnet_wallet()).await; @@ -714,7 +714,7 @@ mod mine_loop_tests { #[traced_test] #[tokio::test] async fn block_timestamp_represents_time_block_found() -> Result<()> { - let network = Network::RegTest; + let network = Network::Regtest; let global_state_lock = mock_genesis_global_state(network, 2, WalletSecret::devnet_wallet()).await; @@ -786,7 +786,7 @@ mod mine_loop_tests { #[traced_test] #[tokio::test] async fn mine_ten_blocks_in_ten_seconds() -> Result<()> { - let network = Network::RegTest; + let network = Network::Regtest; let global_state_lock = mock_genesis_global_state(network, 2, WalletSecret::devnet_wallet()).await; diff --git a/src/models/blockchain/block/block_height.rs b/src/models/blockchain/block/block_height.rs index 31c10f68..eadb7448 100644 --- a/src/models/blockchain/block/block_height.rs +++ b/src/models/blockchain/block/block_height.rs @@ -1,6 +1,7 @@ use std::cmp::Ordering; use std::fmt::Display; use std::ops::Add; +use std::ops::AddAssign; use std::ops::Sub; use get_size::GetSize; @@ -82,6 +83,20 @@ impl Add for BlockHeight { } } +impl Add for BlockHeight { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(BFieldElement::new(self.0.value() + rhs.0.value())) + } +} + +impl AddAssign for BlockHeight { + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + impl Sub for BlockHeight { type Output = i128; diff --git a/src/models/blockchain/block/block_selector.rs b/src/models/blockchain/block/block_selector.rs index 3095d869..1b9edab2 100644 --- a/src/models/blockchain/block/block_selector.rs +++ b/src/models/blockchain/block/block_selector.rs @@ -20,11 +20,11 @@ use serde::Deserialize; use serde::Serialize; use thiserror::Error; -use crate::models::state::GlobalState; use crate::twenty_first::error::TryFromHexDigestError; use crate::twenty_first::math::digest::Digest; use super::block_height::BlockHeight; +use super::traits::BlockchainBlockSelector; /// Provides alternatives for looking up a block. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -104,18 +104,22 @@ impl FromStr for BlockSelector { impl BlockSelector { /// returns Digest for this selector, if it exists. - pub async fn as_digest(&self, state: &GlobalState) -> Option { + pub async fn as_digest(&self, state: &impl BlockchainBlockSelector) -> Option { match self { BlockSelector::Digest(d) => Some(*d), - BlockSelector::Height(h) => { - state - .chain - .archival_state() - .block_height_to_canonical_block_digest(*h, state.chain.light_state().hash()) - .await - } - BlockSelector::Tip => Some(state.chain.light_state().hash()), - BlockSelector::Genesis => Some(state.chain.archival_state().genesis_block().hash()), + BlockSelector::Height(h) => state.height_to_canonical_digest(*h).await, + BlockSelector::Tip => Some(state.tip_digest()), + BlockSelector::Genesis => Some(state.genesis_digest()), + } + } + + /// returns Digest for this selector, if it exists. + pub async fn as_height(&self, state: &impl BlockchainBlockSelector) -> Option { + match self { + BlockSelector::Digest(d) => state.digest_to_canonical_height(*d).await, + BlockSelector::Height(h) => Some(*h), + BlockSelector::Tip => Some(state.tip_height()), + BlockSelector::Genesis => Some(BlockHeight::genesis()), } } } diff --git a/src/models/blockchain/block/mod.rs b/src/models/blockchain/block/mod.rs index 3ac49df0..c26f5ee8 100644 --- a/src/models/blockchain/block/mod.rs +++ b/src/models/blockchain/block/mod.rs @@ -5,6 +5,7 @@ pub mod block_info; pub mod block_kernel; pub mod block_selector; pub mod mutator_set_update; +pub mod traits; pub mod transfer_block; pub mod validity; @@ -58,6 +59,7 @@ use super::transaction::transaction_kernel::TransactionKernel; use super::transaction::utxo::Utxo; use super::transaction::validity::TransactionValidationLogic; use super::transaction::Transaction; +use super::transaction::TxAddressOutput; use super::type_scripts::neptune_coins::NeptuneCoins; use super::type_scripts::time_lock::TimeLock; @@ -313,7 +315,7 @@ impl Block { let header: BlockHeader = BlockHeader { version: BFieldElement::zero(), height: BFieldElement::zero().into(), - prev_block_digest: Default::default(), + prev_block_digest: Self::genesis_prev_block_digest(), timestamp: network.launch_date(), // to be set to something difficult to predict ahead of time nonce: [ @@ -330,7 +332,7 @@ impl Block { Self::new(header, body, BlockType::Genesis) } - fn premine_distribution(_network: Network) -> Vec<(ReceivingAddress, NeptuneCoins)> { + fn premine_distribution(_network: Network) -> Vec { // The premine UTXOs can be hardcoded here. let authority_wallet = WalletSecret::devnet_wallet(); let authority_receiving_address = authority_wallet @@ -400,12 +402,7 @@ impl Block { transaction.kernel.timestamp, ), ); - let new_transaction = self - .kernel - .body - .transaction - .clone() - .merge_with(transaction.clone()); + let new_transaction = self.kernel.body.transaction.clone().merge_with(transaction); // accumulate mutator set updates // Can't use the current mutator sat accumulator because it is in an in-between state. @@ -708,14 +705,19 @@ impl Block { old_block.kernel.header.difficulty - adjustment_u32s } } + + /// returns value of prev_block_digest for the genesis block. + pub fn genesis_prev_block_digest() -> Digest { + Default::default() + } } #[cfg(test)] mod block_tests { + use clap::ValueEnum; use rand::random; use rand::thread_rng; use rand::Rng; - use strum::IntoEnumIterator; use tracing_test::traced_test; use crate::config_models::network::Network; @@ -734,7 +736,7 @@ mod block_tests { let mut rng = thread_rng(); // We need the global state to construct a transaction. This global state // has a wallet which receives a premine-UTXO. - let network = Network::RegTest; + let network = Network::Regtest; let mut global_state_lock = mock_genesis_global_state(network, 2, WalletSecret::devnet_wallet()).await; let spending_key = global_state_lock @@ -795,7 +797,7 @@ mod block_tests { #[test] fn test_difficulty_control_matches() { let mut rng = thread_rng(); - let network = Network::RegTest; + let network = Network::Regtest; let a_wallet_secret = WalletSecret::new_random(); let a_recipient_address = a_wallet_secret @@ -894,7 +896,7 @@ mod block_tests { #[test] fn block_with_wrong_mmra_is_invalid() { let mut rng = thread_rng(); - let network = Network::RegTest; + let network = Network::Regtest; let genesis_block = Block::genesis_block(network); let a_wallet_secret = WalletSecret::new_random(); @@ -914,7 +916,7 @@ mod block_tests { #[test] fn block_with_far_future_timestamp_is_invalid() { let mut rng = thread_rng(); - let network = Network::RegTest; + let network = Network::Regtest; let genesis_block = Block::genesis_block(network); let mut now = genesis_block.kernel.header.timestamp; @@ -951,7 +953,7 @@ mod block_tests { #[tokio::test] async fn can_prove_block_ancestry() { let mut rng = thread_rng(); - let network = Network::RegTest; + let network = Network::Regtest; let genesis_block = Block::genesis_block(network); let mut blocks = vec![]; blocks.push(genesis_block.clone()); @@ -1012,9 +1014,9 @@ mod block_tests { // 831600 = 42000000 * 0.0198 // where 42000000 is the asymptotical limit of the token supply // and 1.98% is the relative size of the premine - for network in Network::iter() { + for network in Network::value_variants() { let premine_max_size = NeptuneCoins::new(831600); - let total_premine = Block::premine_distribution(network) + let total_premine = Block::premine_distribution(*network) .iter() .map(|(_receiving_address, amount)| *amount) .sum::(); @@ -1037,7 +1039,7 @@ mod block_tests { // Arc>> would link the digest in the clone #[test] fn clone_and_modify() { - let gblock = Block::genesis_block(Network::RegTest); + let gblock = Block::genesis_block(Network::Regtest); let g_hash = gblock.hash(); let mut g2 = gblock.clone(); @@ -1052,7 +1054,7 @@ mod block_tests { // test: verify digest is correct after Block::new(). #[test] fn new() { - let gblock = Block::genesis_block(Network::RegTest); + let gblock = Block::genesis_block(Network::Regtest); let g2 = gblock.clone(); let block = Block::new(g2.kernel.header, g2.kernel.body, g2.block_type); @@ -1062,7 +1064,7 @@ mod block_tests { // test: verify digest changes after nonce is updated. #[test] fn set_header_nonce() { - let gblock = Block::genesis_block(Network::RegTest); + let gblock = Block::genesis_block(Network::Regtest); let mut rng = thread_rng(); let mut new_block = gblock.clone(); @@ -1073,7 +1075,7 @@ mod block_tests { // test: verify set_block() copies source digest #[test] fn set_block() { - let gblock = Block::genesis_block(Network::RegTest); + let gblock = Block::genesis_block(Network::Regtest); let mut rng = thread_rng(); let mut unique_block = gblock.clone(); @@ -1093,7 +1095,7 @@ mod block_tests { // note: we have to generate a block becau // TransferBlock::into() will panic if it // encounters the genesis block. let global_state_lock = - mock_genesis_global_state(Network::RegTest, 2, WalletSecret::devnet_wallet()).await; + mock_genesis_global_state(Network::Regtest, 2, WalletSecret::devnet_wallet()).await; let spending_key = global_state_lock .lock_guard() .await @@ -1103,7 +1105,7 @@ mod block_tests { let address = spending_key.to_address(); let mut rng = thread_rng(); - let gblock = Block::genesis_block(Network::RegTest); + let gblock = Block::genesis_block(Network::Regtest); let (source_block, _, _) = make_mock_block(&gblock, None, address, rng.gen()); @@ -1115,7 +1117,7 @@ mod block_tests { // test: verify digest is correct after deserializing #[test] fn deserialize() { - let gblock = Block::genesis_block(Network::RegTest); + let gblock = Block::genesis_block(Network::Regtest); let bytes = bincode::serialize(&gblock).unwrap(); let block: Block = bincode::deserialize(&bytes).unwrap(); @@ -1137,7 +1139,7 @@ mod block_tests { // round trip. #[test] fn bfieldcodec_encode_and_decode() { - let gblock = Block::genesis_block(Network::RegTest); + let gblock = Block::genesis_block(Network::Regtest); let encoded: Vec = gblock.encode(); let decoded: Block = *Block::decode(&encoded).unwrap(); diff --git a/src/models/blockchain/block/traits.rs b/src/models/blockchain/block/traits.rs new file mode 100644 index 00000000..ebbad269 --- /dev/null +++ b/src/models/blockchain/block/traits.rs @@ -0,0 +1,31 @@ +//! traits for working with blocks + +use tasm_lib::Digest; + +use super::block_height::BlockHeight; + +/// an interface for any type that provides data to +/// [BlockSelector](super::block_selector::BlockSelector) to read from the +/// blockchain. +/// +/// note: this trait enables BlockSelector to abstract over BlockchainState and +/// &ArchivalState. The latter is necessary for use in ArchivalState methods +/// which take &self. +pub trait BlockchainBlockSelector { + /// returns the tip digest + fn tip_digest(&self) -> Digest; + + /// returns the tip height + fn tip_height(&self) -> BlockHeight; + + /// returns genesis digest. + fn genesis_digest(&self) -> Digest; + + // returns digest of canonical block at the given height + #[allow(async_fn_in_trait)] + async fn height_to_canonical_digest(&self, h: BlockHeight) -> Option; + + // returns height of canonical block with the given digest + #[allow(async_fn_in_trait)] + async fn digest_to_canonical_height(&self, d: Digest) -> Option; +} diff --git a/src/models/blockchain/transaction/mod.rs b/src/models/blockchain/transaction/mod.rs index 8f349a94..bf0047e8 100644 --- a/src/models/blockchain/transaction/mod.rs +++ b/src/models/blockchain/transaction/mod.rs @@ -7,6 +7,7 @@ pub mod validity; mod transaction_input; mod transaction_output; +mod transaction_params; use std::cmp::max; use std::collections::HashMap; @@ -15,10 +16,13 @@ use std::hash::Hasher as StdHasher; pub use transaction_input::TxInput; pub use transaction_input::TxInputList; +pub use transaction_output::OwnedUtxoNotifyMethod; +pub use transaction_output::TxAddressOutput; pub use transaction_output::TxOutput; pub use transaction_output::TxOutputList; +pub use transaction_output::UnownedUtxoNotifyMethod; pub use transaction_output::UtxoNotification; -pub use transaction_output::UtxoNotifyMethod; +pub use transaction_params::TxParams; use anyhow::bail; use anyhow::Result; @@ -45,12 +49,15 @@ use validity::TransactionValidationLogic; use crate::models::blockchain::block::mutator_set_update::MutatorSetUpdate; use crate::models::consensus::mast_hash::MastHash; +use crate::models::consensus::timestamp::Timestamp; use crate::models::consensus::ValidityTree; use crate::models::consensus::WitnessType; use crate::models::state::wallet::expected_utxo::ExpectedUtxo; +use crate::models::state::wallet::expected_utxo::UtxoNotifier; use crate::prelude::triton_vm; use crate::prelude::twenty_first; use crate::util_types::mutator_set::addition_record::AdditionRecord; +use crate::util_types::mutator_set::commit; 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; @@ -69,7 +76,6 @@ use super::type_scripts::TypeScript; /// 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, @@ -78,7 +84,6 @@ pub struct AnnouncedUtxo { 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, @@ -86,6 +91,32 @@ impl From<&ExpectedUtxo> for AnnouncedUtxo { } } +impl From<(AnnouncedUtxo, UtxoNotifier)> for ExpectedUtxo { + fn from(inputs: (AnnouncedUtxo, UtxoNotifier)) -> Self { + let (au, un) = inputs; + Self { + addition_record: au.addition_record(), + utxo: au.utxo, + sender_randomness: au.sender_randomness, + receiver_preimage: au.receiver_preimage, + received_from: un, + notification_received: Timestamp::now(), + mined_in_block: None, + } + } +} + +impl AnnouncedUtxo { + pub fn addition_record(&self) -> AdditionRecord { + let receiver_digest = self.receiver_preimage.hash::(); + commit( + Hash::hash(&self.utxo), + self.sender_randomness, + receiver_digest, + ) + } +} + /// represents arbitrary data that can be stored in a transaction on the public blockchain /// /// initially these are used for transmitting encrypted secrets necessary diff --git a/src/models/blockchain/transaction/transaction_input.rs b/src/models/blockchain/transaction/transaction_input.rs index 5192e5a0..b43011f3 100644 --- a/src/models/blockchain/transaction/transaction_input.rs +++ b/src/models/blockchain/transaction/transaction_input.rs @@ -4,26 +4,46 @@ 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::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; +use serde::Deserialize; +use serde::Serialize; use std::ops::Deref; use std::ops::DerefMut; +use tasm_lib::triton_vm::prelude::BFieldCodec; +use tasm_lib::triton_vm::prelude::BFieldElement; use tasm_lib::twenty_first::prelude::AlgebraicHasher; +use tasm_lib::Digest; /// represents a transaction input, as accepted by /// [create_transaction()](crate::models::state::GlobalState::create_transaction()) -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct TxInput { - pub spending_key: SpendingKey, + pub unlock_key: Digest, pub utxo: Utxo, pub lock_script: LockScript, pub ms_membership_proof: MsMembershipProof, } +#[cfg(test)] +impl TxInput { + pub fn new_random(amount: NeptuneCoins) -> Self { + use crate::models::state::wallet::address::generation_address::GenerationSpendingKey; + use crate::util_types::mutator_set::ms_membership_proof::pseudorandom_mutator_set_membership_proof; + + let lock_script = LockScript::anyone_can_spend(); + Self { + unlock_key: GenerationSpendingKey::derive_from_seed(rand::random()).unlock_key, + utxo: Utxo::new_native_coin(lock_script.clone(), amount), + lock_script, + ms_membership_proof: pseudorandom_mutator_set_membership_proof(rand::random()), + } + } +} + /// Represents a list of [TxInput] -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct TxInputList(Vec); impl Deref for TxInputList { @@ -40,6 +60,12 @@ impl DerefMut for TxInputList { } } +impl From for TxInputList { + fn from(t: TxInput) -> Self { + Self(vec![t]) + } +} + impl From> for TxInputList { fn from(v: Vec) -> Self { Self(v) @@ -100,14 +126,19 @@ impl TxInputList { self.lock_scripts_iter().into_iter().collect() } - /// retrieves spending keys - pub fn spending_keys_iter(&self) -> impl IntoIterator + '_ { - self.0.iter().map(|u| u.spending_key) + /// retrieves unlock keys + pub fn unlock_keys_iter(&self) -> impl Iterator + '_ { + self.0.iter().map(|u| u.unlock_key) + } + + /// retrieves unlock keys + pub fn unlock_keys(&self) -> Vec { + self.unlock_keys_iter().collect() } - /// retrieves spending keys - pub fn spending_keys(&self) -> Vec { - self.spending_keys_iter().into_iter().collect() + /// retrieves unlock keys as lock script witnesses + pub fn lock_script_witnesses(&self) -> Vec> { + self.unlock_keys_iter().map(|uk| uk.encode()).collect() } /// retrieves membership proofs diff --git a/src/models/blockchain/transaction/transaction_kernel.rs b/src/models/blockchain/transaction/transaction_kernel.rs index 6085d880..79d81a84 100644 --- a/src/models/blockchain/transaction/transaction_kernel.rs +++ b/src/models/blockchain/transaction/transaction_kernel.rs @@ -237,7 +237,7 @@ pub mod transaction_kernel_tests { canonical_commitment: random(), }], public_announcements: Default::default(), - fee: NeptuneCoins::one(), + fee: NeptuneCoins::one_nau(), coinbase: None, timestamp: Default::default(), mutator_set_hash: rng.gen::(), diff --git a/src/models/blockchain/transaction/transaction_output.rs b/src/models/blockchain/transaction/transaction_output.rs index 5b9055e5..5c21056a 100644 --- a/src/models/blockchain/transaction/transaction_output.rs +++ b/src/models/blockchain/transaction/transaction_output.rs @@ -8,6 +8,8 @@ use crate::models::state::wallet::address::ReceivingAddress; use crate::models::state::wallet::address::SpendingKey; use crate::models::state::wallet::expected_utxo::ExpectedUtxo; use crate::models::state::wallet::expected_utxo::UtxoNotifier; +use crate::models::state::wallet::utxo_transfer::UtxoTransfer; +use crate::models::state::wallet::utxo_transfer::UtxoTransferEncrypted; use crate::models::state::wallet::wallet_state::WalletState; use crate::prelude::twenty_first::math::digest::Digest; use crate::prelude::twenty_first::util_types::algebraic_hasher::AlgebraicHasher; @@ -19,16 +21,35 @@ use serde::Serialize; use std::ops::Deref; use std::ops::DerefMut; -/// enumerates how utxos should be transferred. +pub type TxAddressOutput = (ReceivingAddress, NeptuneCoins); + +/// enumerates how self-owned utxos should be transferred (back) to our own wallet. /// -/// see also: [UtxoNotification] -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub enum UtxoNotifyMethod { +/// see also: [UnownedUtxoNotifyMethod], [UtxoNotification] +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, clap::ValueEnum)] +pub enum OwnedUtxoNotifyMethod { + #[default] /// the utxo notification should be transferred to recipient encrypted on the blockchain OnChain, - /// the utxo notification should be transferred to recipient off the blockchain + /// the utxo notification should be transferred to recipient off the blockchain by neptune-core OffChain, + + /// the utxo notification should be transferred to recipient off the blockchain external to neptune-core + OffChainSerialized, +} + +/// enumerates how utxos should be transferred to third parties. +/// +/// see also: [UnownedUtxoNotifyMethod], [UtxoNotification] +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, clap::ValueEnum)] +pub enum UnownedUtxoNotifyMethod { + #[default] + /// the utxo notification should be transferred to recipient encrypted on the blockchain + OnChain, + + /// the utxo notification should be transferred to recipient off the blockchain external to neptune-core + OffChainSerialized, } /// enumerates utxo transfer methods with payloads @@ -36,28 +57,25 @@ pub enum UtxoNotifyMethod { /// [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) +/// see also: [UnownedUtxoNotifyMethod], [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. +/// perhaps PublicAnnouncement should be used for `OffChain` +/// and `OffChainSerialized`. to consolidate code/logic. /// /// see comment for: [TxOutput::auto()] /// -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum UtxoNotification { - /// the utxo notification should be transferred to recipient on the blockchain as a [PublicAnnouncement] + /// the utxo notification should be transferred to recipient on-chain as a [PublicAnnouncement] OnChain(PublicAnnouncement), - /// the utxo notification should be transferred to recipient off the blockchain as an [ExpectedUtxo] + /// the utxo notification should be transferred to recipient off-chain by neptune-core as an [ExpectedUtxo] OffChain(Box), + + /// the utxo notification should be transferred to recipient off-chain external to neptune-core + OffChainSerialized(UtxoTransferEncrypted), } /// represents a transaction output, as accepted by @@ -65,7 +83,7 @@ pub enum UtxoNotification { /// /// Contains data that a UTXO recipient requires in order to be notified about /// and claim a given UTXO -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct TxOutput { pub utxo: Utxo, pub sender_randomness: Digest, @@ -98,15 +116,17 @@ impl From<&TxOutput> for AdditionRecord { impl TxOutput { /// 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] cannot be claimed by our wallet then + /// `owned_utxo_notify_method` dictates the behavior: + /// * `OnChain` results in blockchain transfer via whichever address type is provided. + /// * `OffChainSerialized` results in transfer outside of neptune-core. /// /// 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. + /// * `OffChain` results in local state transfer via whichever address type is provided. + /// * `OffChainSerialized` results in transfer outside of neptune-core. /// /// design decision: we do not return any error if a pub-key is used for /// onchain notification of an owned utxo. @@ -129,21 +149,13 @@ impl TxOutput { /// 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. - /// pub fn auto( wallet_state: &WalletState, address: &ReceivingAddress, amount: NeptuneCoins, sender_randomness: Digest, - owned_utxo_notify_method: UtxoNotifyMethod, + owned_utxo_notify_method: OwnedUtxoNotifyMethod, + unowned_utxo_notify_method: UnownedUtxoNotifyMethod, ) -> Result { let onchain = || -> Result { let utxo = Utxo::new_native_coin(address.lock_script(), amount); @@ -161,14 +173,30 @@ impl TxOutput { Self::offchain(utxo, sender_randomness, key.privacy_preimage()) }; + let offchain_serialized = || -> Result { + let utxo = Utxo::new_native_coin(address.lock_script(), amount); + let utxo_transfer_encrypted = + UtxoTransfer::new(utxo.clone(), sender_randomness).encrypt_to_address(address)?; + Ok(Self::offchain_serialized( + utxo, + sender_randomness, + address.privacy_digest(), + utxo_transfer_encrypted, + )) + }; + let utxo = Utxo::new_native_coin(address.lock_script(), amount); - let utxo_wallet_key = wallet_state.find_spending_key_for_utxo(&utxo); + let utxo_wallet_key = wallet_state.find_known_spending_key_for_utxo(&utxo); let tx_output = match utxo_wallet_key { - None => onchain()?, + None => match unowned_utxo_notify_method { + UnownedUtxoNotifyMethod::OnChain => onchain()?, + UnownedUtxoNotifyMethod::OffChainSerialized => offchain_serialized()?, + }, Some(key) => match owned_utxo_notify_method { - UtxoNotifyMethod::OnChain => onchain()?, - UtxoNotifyMethod::OffChain => offchain(key), + OwnedUtxoNotifyMethod::OnChain => onchain()?, + OwnedUtxoNotifyMethod::OffChain => offchain(key), + OwnedUtxoNotifyMethod::OffChainSerialized => offchain_serialized()?, }, }; @@ -209,6 +237,20 @@ impl TxOutput { .into() } + pub fn offchain_serialized( + utxo: Utxo, + sender_randomness: Digest, + receiver_privacy_digest: Digest, + utxo_transfer_encrypted: UtxoTransferEncrypted, + ) -> Self { + Self { + utxo, + sender_randomness, + receiver_privacy_digest, + utxo_notification: UtxoNotification::OffChainSerialized(utxo_transfer_encrypted), + } + } + // only for legacy tests #[cfg(test)] pub fn fake_address( @@ -237,10 +279,19 @@ impl TxOutput { pub fn random(utxo: Utxo) -> Self { Self::fake_address(utxo, rand::random(), rand::random()) } + + // only for legacy tests + #[cfg(test)] + pub fn new_random(amount: NeptuneCoins) -> Self { + use super::utxo::LockScript; + + let utxo = Utxo::new_native_coin(LockScript::anyone_can_spend(), amount); + Self::fake_address(utxo, rand::random(), rand::random()) + } } /// Represents a list of [TxOutput] -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct TxOutputList(Vec); impl Deref for TxOutputList { @@ -257,6 +308,12 @@ impl DerefMut for TxOutputList { } } +impl From for TxOutputList { + fn from(v: TxOutput) -> Self { + Self(vec![v]) + } +} + impl From> for TxOutputList { fn from(v: Vec) -> Self { Self(v) @@ -344,6 +401,14 @@ impl TxOutputList { .any(|u| matches!(&u.utxo_notification, UtxoNotification::OffChain(_))) } + /// retrieves expected_utxos from possible sub-set of the list + pub fn utxo_transfer_iter(&self) -> impl Iterator + '_ { + self.0.iter().filter_map(|u| match &u.utxo_notification { + UtxoNotification::OffChainSerialized(ut) => Some(ut.clone()), + _ => None, + }) + } + /// retrieves expected_utxos from possible sub-set of the list pub fn expected_utxos(&self) -> Vec { self.expected_utxos_iter().collect() @@ -359,12 +424,13 @@ mod tests { use crate::models::state::wallet::address::KeyType; use crate::models::state::wallet::WalletSecret; use crate::tests::shared::mock_genesis_global_state; + use clap::ValueEnum; use rand::Rng; #[tokio::test] async fn test_utxoreceiver_auto_not_owned_output() -> Result<()> { let global_state_lock = - mock_genesis_global_state(Network::RegTest, 2, WalletSecret::devnet_wallet()).await; + mock_genesis_global_state(Network::Regtest, 2, WalletSecret::devnet_wallet()).await; let state = global_state_lock.lock_guard().await; let block_height = state.chain.light_state().header().height; @@ -374,7 +440,7 @@ mod tests { let seed: Digest = rng.gen(); let address = GenerationReceivingAddress::derive_from_seed(seed); - let amount = NeptuneCoins::one(); + let amount = NeptuneCoins::one_nau(); let utxo = Utxo::new_native_coin(address.lock_script(), amount); let sender_randomness = state @@ -382,13 +448,14 @@ mod tests { .wallet_secret .generate_sender_randomness(block_height, address.privacy_digest); - for utxo_notify_method in [UtxoNotifyMethod::OffChain, UtxoNotifyMethod::OnChain] { + for owned_utxo_notify_method in OwnedUtxoNotifyMethod::value_variants() { let utxo_receiver = TxOutput::auto( &state.wallet_state, &address.into(), amount, sender_randomness, - utxo_notify_method, // how to notify of owned utxos. + *owned_utxo_notify_method, // how to notify of owned utxos. + UnownedUtxoNotifyMethod::OnChain, )?; // we should have OnChain transfer regardless of owned_transfer_method setting @@ -411,7 +478,7 @@ mod tests { #[tokio::test] async fn test_utxoreceiver_auto_owned_output() -> Result<()> { let mut global_state_lock = - mock_genesis_global_state(Network::RegTest, 2, WalletSecret::devnet_wallet()).await; + mock_genesis_global_state(Network::Regtest, 2, WalletSecret::devnet_wallet()).await; // obtain next unused receiving address from our wallet. let spending_key_gen = global_state_lock @@ -432,11 +499,15 @@ mod tests { let state = global_state_lock.lock_guard().await; let block_height = state.chain.light_state().header().height; - let amount = NeptuneCoins::one(); + let amount = NeptuneCoins::one_nau(); - for (transfer_method, address) in [ - (UtxoNotifyMethod::OffChain, address_gen.clone()), - (UtxoNotifyMethod::OnChain, address_sym.clone()), + for (owned_utxo_notify_method, address) in [ + (OwnedUtxoNotifyMethod::OffChain, address_gen.clone()), + ( + OwnedUtxoNotifyMethod::OffChainSerialized, + address_gen.clone(), + ), + (OwnedUtxoNotifyMethod::OnChain, address_sym.clone()), ] { let utxo = Utxo::new_native_coin(address.lock_script(), amount); let sender_randomness = state @@ -449,20 +520,29 @@ mod tests { &address, amount, sender_randomness, - transfer_method, // how to notify of owned utxos. + owned_utxo_notify_method, // how to notify of owned utxos. + UnownedUtxoNotifyMethod::OnChain, )?; let transfer_is_correct = match utxo_receiver.utxo_notification { UtxoNotification::OffChain(_) => { - matches!(transfer_method, UtxoNotifyMethod::OffChain) + matches!(owned_utxo_notify_method, OwnedUtxoNotifyMethod::OffChain) + } + UtxoNotification::OffChainSerialized(_) => { + matches!( + owned_utxo_notify_method, + OwnedUtxoNotifyMethod::OffChainSerialized + ) } - UtxoNotification::OnChain(ref pa) => match transfer_method { - UtxoNotifyMethod::OnChain => address.matches_public_announcement_key_type(pa), + UtxoNotification::OnChain(ref pa) => match owned_utxo_notify_method { + OwnedUtxoNotifyMethod::OnChain => { + address.matches_public_announcement_key_type(pa) + } _ => false, }, }; - println!("owned_transfer_method: {:#?}", transfer_method); + println!("owned_transfer_method: {:#?}", owned_utxo_notify_method); println!("utxo_transfer: {:#?}", utxo_receiver.utxo_notification); assert!(transfer_is_correct); diff --git a/src/models/blockchain/transaction/transaction_params.rs b/src/models/blockchain/transaction/transaction_params.rs new file mode 100644 index 00000000..133a740f --- /dev/null +++ b/src/models/blockchain/transaction/transaction_params.rs @@ -0,0 +1,253 @@ +//! This module implements TxParams which is used as input to +//! create_transaction() and the send() rpc. +use num_traits::CheckedSub; +use serde::Deserialize; +use serde::Serialize; +use thiserror::Error; + +use crate::models::blockchain::type_scripts::neptune_coins::NeptuneCoins; +use crate::models::consensus::timestamp::Timestamp; + +use super::TxInputList; +use super::TxOutputList; + +/// represents validation errors when constructing TxParams +#[derive(Debug, Clone, Error)] +pub enum TxParamsError { + #[error("inputs ({inputs_sum}) is less than outputs ({outputs_sum})")] + InsufficientInputs { + inputs_sum: NeptuneCoins, + outputs_sum: NeptuneCoins, + }, + + #[error("negative amount is not permitted for inputs or outputs")] + NegativeAmount, +} + +// About serialization+validation +// +// the goal is to validate inside the impl Deserialize to ensure +// correct-by-construction using the "parse, don't validate" design philosophy. +// +// unfortunately serde does not yet directly support validating when using +// derive Deserialize. So a workaround pattern is to create a shadow +// struct with the same fields that gets deserialized without validation +// and then use try_from to validate and construct the target. +// +// see: https://github.com/serde-rs/serde/issues/642#issuecomment-683276351 + +/// In RPC usage TxParams will typically be created by the generate_tx_params() +/// RPC and then used as an argument to the send() RPC. For the send RPC, it +/// is an untrusted data source. +/// +/// Basic validation of input/output amounts occurs when TxParams is constructed +/// including via deserialization (on both client and server). +/// +/// This means that validation occurs on the client as well as on the server +/// before create_transaction() is ever called. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "TxParamsShadow")] +pub struct TxParams { + tx_input_list: TxInputList, + tx_output_list: TxOutputList, + timestamp: Timestamp, +} + +// note: this only exists to get deserialized without validation. we also +// derive Serialize for unit tests (only) in order to simulate invalid input +// data. +#[cfg_attr(test, derive(Serialize))] +#[derive(Deserialize)] +struct TxParamsShadow { + tx_input_list: TxInputList, + tx_output_list: TxOutputList, + timestamp: Timestamp, +} + +impl std::convert::TryFrom for TxParams { + type Error = TxParamsError; + + fn try_from(s: TxParamsShadow) -> Result { + Self::new_with_timestamp(s.tx_input_list, s.tx_output_list, s.timestamp) + } +} + +impl TxParams { + /// construct a new TxParams using the current time + pub fn new(tx_inputs: TxInputList, tx_outputs: TxOutputList) -> Result { + Self::new_with_timestamp(tx_inputs, tx_outputs, Timestamp::now()) + } + + /// construct a new TxParams with a custom timestamp + pub fn new_with_timestamp( + tx_input_list: TxInputList, + tx_output_list: TxOutputList, + timestamp: Timestamp, + ) -> Result { + // validate that all input and output amounts are non-negative. (zero is allowed) + for amount in tx_input_list + .iter() + .map(|i| i.utxo.get_native_currency_amount()) + .chain( + tx_output_list + .iter() + .map(|o| o.utxo.get_native_currency_amount()), + ) + { + if amount.is_negative() { + return Err(TxParamsError::NegativeAmount); + } + } + + if tx_input_list.total_native_coins() < tx_output_list.total_native_coins() { + return Err(TxParamsError::InsufficientInputs { + inputs_sum: tx_input_list.total_native_coins(), + outputs_sum: tx_output_list.total_native_coins(), + }); + } + + // todo: consider validating that all inputs are spendable now. + // todo: any other validations? + + Ok(Self { + tx_input_list, + tx_output_list, + timestamp, + }) + } + + /// return the fee amount which is sum(inputs) - sum(outputs) + /// + /// fee will always be >= 0, guaranteed by [Self::new()] + pub fn fee(&self) -> NeptuneCoins { + // note: the unwrap will never fail because fee always >= 0, else a serious bug. + self.tx_input_list + .total_native_coins() + .checked_sub(&self.tx_output_list.total_native_coins()) + .unwrap() + } + + /// get the transaction inputs + pub fn tx_input_list(&self) -> &TxInputList { + &self.tx_input_list + } + + /// get the transaction outputs + pub fn tx_output_list(&self) -> &TxOutputList { + &self.tx_output_list + } + + /// get the timestamp + pub fn timestamp(&self) -> &Timestamp { + &self.timestamp + } +} + +#[cfg(test)] +mod tests { + use crate::models::blockchain::transaction::TxInput; + use crate::models::blockchain::transaction::TxOutput; + + use super::*; + + #[test] + pub fn validate_insufficient_inputs() -> anyhow::Result<()> { + let tx_input = TxInput::new_random(NeptuneCoins::new(15)); + let tx_output = TxOutput::new_random(NeptuneCoins::new(20)); + + // test TxParams::new() + assert!(matches!( + TxParams::new(tx_input.clone().into(), tx_output.clone().into()), + Err(TxParamsError::InsufficientInputs { .. }) + )); + + // test TxParams::new_with_timestamp() + assert!(matches!( + TxParams::new_with_timestamp( + tx_input.clone().into(), + tx_output.clone().into(), + Timestamp::now() + ), + Err(TxParamsError::InsufficientInputs { .. }) + )); + + // test TxParams::deserialize() + { + let serialized = bincode::serialize(&TxParamsShadow { + tx_input_list: tx_input.clone().into(), + tx_output_list: tx_output.clone().into(), + timestamp: Timestamp::now(), + })?; + + let result = bincode::deserialize::(&serialized); + assert!(matches!( + *result.unwrap_err(), + bincode::ErrorKind::Custom(s) if s == TxParams::new(tx_input.into(), tx_output.into()).unwrap_err().to_string() + )); + } + + Ok(()) + } + + // validates that a NegativeAmount error occurs if inputs has a negative-amount entry. + // checks TxParams::new(), TxParams::new_with_timestamp() and TxParams::deserialize() + #[test] + pub fn validate_negative_input_amount() -> anyhow::Result<()> { + worker::validate_negative_amount("-5".parse().unwrap(), NeptuneCoins::new(15)) + } + + // validates that a NegativeAmount error occurs if outputs has a negative-amount entry. + // checks TxParams::new(), TxParams::new_with_timestamp() and TxParams::deserialize() + #[test] + pub fn validate_negative_output_amount() -> anyhow::Result<()> { + worker::validate_negative_amount(NeptuneCoins::new(15), "-5".parse().unwrap()) + } + + mod worker { + use super::*; + + // validates that a NegativeAmount error occurs if inputs or outputs has a negative-amount entry. + // requires that caller pass a negative value for at least one arg. + // checks TxParams::new(), TxParams::new_with_timestamp() and TxParams::deserialize() + pub fn validate_negative_amount( + input_amt: NeptuneCoins, + output_amt: NeptuneCoins, + ) -> anyhow::Result<()> { + let tx_input = TxInput::new_random(input_amt); + let tx_output = TxOutput::new_random(output_amt); + + // test TxParams::new() + assert!(matches!( + TxParams::new(tx_input.clone().into(), tx_output.clone().into()), + Err(TxParamsError::NegativeAmount { .. }) + )); + + // test TxParams::new_with_timestamp() + assert!(matches!( + TxParams::new_with_timestamp( + tx_input.clone().into(), + tx_output.clone().into(), + Timestamp::now() + ), + Err(TxParamsError::NegativeAmount) + )); + + // test TxParams::deserialize() + { + let serialized = bincode::serialize(&TxParamsShadow { + tx_input_list: tx_input.clone().into(), + tx_output_list: tx_output.clone().into(), + timestamp: Timestamp::now(), + })?; + + let result = bincode::deserialize::(&serialized); + assert!(matches!( + *result.unwrap_err(), + bincode::ErrorKind::Custom(s) if s == TxParams::new(tx_input.into(), tx_output.into()).unwrap_err().to_string() + )); + } + + Ok(()) + } + } +} diff --git a/src/models/blockchain/type_scripts/neptune_coins.rs b/src/models/blockchain/type_scripts/neptune_coins.rs index d8e46ef6..b45a375d 100644 --- a/src/models/blockchain/type_scripts/neptune_coins.rs +++ b/src/models/blockchain/type_scripts/neptune_coins.rs @@ -65,12 +65,12 @@ impl NeptuneCoins { product } - /// Return the element that corresponds to 1. Use in tests only. - pub fn one() -> NeptuneCoins { + /// Return the element that corresponds to 1 nau. Use in tests only. + pub fn one_nau() -> NeptuneCoins { NeptuneCoins(1u128) } - /// Create an Amount object of the given number of coins. + /// Create an Amount object of the given number of whole coins. pub fn new(num_coins: u32) -> NeptuneCoins { assert!( num_coins <= 42000000, diff --git a/src/models/state/archival_state.rs b/src/models/state/archival_state.rs index b45019a0..26aa24d1 100644 --- a/src/models/state/archival_state.rs +++ b/src/models/state/archival_state.rs @@ -2,6 +2,7 @@ use std::ops::DerefMut; use std::path::PathBuf; use anyhow::Result; +use futures::Stream; use memmap2::MmapOptions; use num_traits::Zero; use tokio::io::AsyncSeekExt; @@ -19,6 +20,8 @@ use crate::database::NeptuneLevelDb; use crate::database::WriteBatchAsync; use crate::models::blockchain::block::block_header::BlockHeader; use crate::models::blockchain::block::block_height::BlockHeight; +use crate::models::blockchain::block::block_selector::BlockSelector; +use crate::models::blockchain::block::traits::BlockchainBlockSelector; use crate::models::blockchain::block::Block; use crate::models::database::BlockFileLocation; use crate::models::database::BlockIndexKey; @@ -62,6 +65,9 @@ pub struct ArchivalState { // this object in a spawned worker task. genesis_block: Box, + /// The tip of canonical chain + tip_block: Box, + // The archival mutator set is persisted to one database that also records a sync label, // which corresponds to the hash of the block to which the mutator set is synced. pub archival_mutator_set: RustyArchivalMutatorSet, @@ -212,7 +218,12 @@ impl ArchivalState { archival_mutator_set.persist().await; } + let tip_block = Self::load_tip(&data_dir, &block_index_db) + .await + .unwrap_or_else(|| genesis_block.clone()); + Self { + tip_block, data_dir, block_index_db, genesis_block, @@ -220,6 +231,29 @@ impl ArchivalState { } } + // loads tip from DB+disk + async fn load_tip( + data_dir: &DataDirectory, + block_index_db: &NeptuneLevelDb, + ) -> Option> { + if let Some(v) = block_index_db.get(BlockIndexKey::BlockTipDigest).await { + if let Some(bv) = block_index_db + .get(BlockIndexKey::Block(v.as_tip_digest())) + .await + { + let block_record = bv.as_block_record(); + return Self::get_block_from_block_record( + Self::block_file_path(data_dir, &block_record), + block_record, + ) + .await + .map(Box::new) + .ok(); + } + } + None + } + pub fn genesis_block(&self) -> &Block { &self.genesis_block } @@ -347,15 +381,20 @@ impl ArchivalState { self.block_index_db.batch_write(batch).await; + self.tip_block = Box::new(new_block.to_owned()); + Ok(()) } - async fn get_block_from_block_record(&self, block_record: BlockRecord) -> Result { - // Get path of file for block - let block_file_path: PathBuf = self - .data_dir - .block_file_path(block_record.file_location.file_index); + // Get path of file for block + fn block_file_path(data_dir: &DataDirectory, block_record: &BlockRecord) -> PathBuf { + data_dir.block_file_path(block_record.file_location.file_index) + } + async fn get_block_from_block_record( + block_file_path: PathBuf, + block_record: BlockRecord, + ) -> Result { // Open file as read-only let block_file: tokio::fs::File = tokio::fs::OpenOptions::new() .read(true) @@ -379,61 +418,141 @@ impl ArchivalState { .await? } - /// Return the latest block that was stored to disk. If no block has been stored to disk, i.e. - /// if tip is genesis, then `None` is returned - async fn get_tip_from_disk(&self) -> Result> { - let tip_digest = self.block_index_db.get(BlockIndexKey::BlockTipDigest).await; - let tip_digest: Digest = match tip_digest { - Some(digest) => digest.as_tip_digest(), - None => return Ok(None), - }; + /// returns a [Stream] of blocks from `oldest` to `newest` in ascending order + /// + /// This method provides an async "iterator" for canonical blocks. + /// + /// if `oldest` or `newest` does not refer to a canonical block then + /// the returned stream will not yield any blocks. + /// + /// perf: + /// + /// this method returns right away. no blocks are retrieved until the + /// caller iterates over the stream. + /// + /// the stream returned from [Self::canonical_block_stream_desc()] should be + /// faster because it can walk the chain backward using the + /// prev_block_digest in each block's header. + /// + /// In contrast this stream must query the Db for each iteration to map a + /// height to the canonical block. + pub async fn canonical_block_stream_asc( + &self, + oldest: BlockSelector, + newest: BlockSelector, + ) -> impl Stream> + '_ { + let (mut iter_height, newest_height) = + match (oldest.as_height(self).await, newest.as_height(self).await) { + (Some(o), Some(n)) => (o, n), + _ => (1.into(), 0.into()), // 1 > 0, so loop exits immediately. + }; - let tip_block_record: BlockRecord = self - .block_index_db - .get(BlockIndexKey::Block(tip_digest)) - .await - .unwrap() - .as_block_record(); + async_stream::stream! { + while iter_height <= newest_height { + let block = self.get_block(BlockSelector::Height(iter_height).as_digest(self).await.unwrap()).await.unwrap().unwrap(); + iter_height += 1.into(); + yield Box::new(block); + } + } + } - let block: Block = self.get_block_from_block_record(tip_block_record).await?; + /// returns a [Stream] of blocks from `oldest` to `newest` in descending order + /// + /// This method provides an async "iterator" for canonical blocks. + /// + /// if `oldest` or `newest` does not refer to a canonical block then + /// the returned stream will not yield any blocks. + /// + /// perf: + /// + /// this method returns right away. no blocks are retrieved until the + /// caller iterates over the stream. + /// + /// the returned stream walks the chain backward using the prev_block_digest + /// in each block's header. + /// + /// In contrast the stream returned from [Self::canonical_block_stream_asc] + /// must query the Db for each iteration to map a height to the canonical + /// block. + pub async fn canonical_block_stream_desc( + &self, + oldest: BlockSelector, + newest: BlockSelector, + ) -> impl Stream> + '_ { + let (oldest_digest, mut iter_digest) = + match (oldest.as_digest(self).await, newest.as_digest(self).await) { + (Some(o), Some(n)) => (o, n), + _ => ( + // so loop will exit immediately on failure. + Block::genesis_prev_block_digest(), + Block::genesis_prev_block_digest(), + ), + }; - Ok(Some(block)) + async_stream::stream! { + while iter_digest != oldest_digest && iter_digest != Block::genesis_prev_block_digest() { + let block = self.get_block(iter_digest).await.unwrap().unwrap(); + iter_digest = block.header().prev_block_digest; + yield Box::new(block); + } + } + } + + /// searches from tip to `oldest` to find a block containing `output` + pub async fn find_canonical_block_with_output( + &self, + output: AdditionRecord, + oldest: BlockSelector, + ) -> Option { + let oldest_digest = oldest.as_digest(self).await?; + let mut block = self.tip().to_owned(); + + loop { + if block + .body() + .transaction + .kernel + .outputs + .iter() + .any(|ar| *ar == output) + { + break Some(block); + } + + if block.hash() == oldest_digest { + break None; + } + + block = self + .get_block(block.header().prev_block_digest) + .await + .ok()??; + } + } + + /// Return the latest block that was stored to disk. If no block has been stored to disk, i.e. + /// if tip is genesis, then `None` is returned + #[cfg(test)] + async fn get_tip_from_disk(&self) -> Option> { + Self::load_tip(&self.data_dir, &self.block_index_db).await } /// Return latest block from database, or genesis block if no other block /// is known. - pub async fn get_tip(&self) -> Block { - let lookup_res_info: Option = self - .get_tip_from_disk() - .await - .expect("Failed to read block from disk"); + pub fn tip(&self) -> &Block { + &self.tip_block + } - match lookup_res_info { - None => *self.genesis_block.clone(), - Some(block) => block, - } + /// returns mutable reference to tip block + pub(crate) fn tip_mut(&mut self) -> &mut Block { + &mut self.tip_block } /// Return parent of tip block. Returns `None` iff tip is genesis block. pub async fn get_tip_parent(&self) -> Option { - let tip_digest = self - .block_index_db - .get(BlockIndexKey::BlockTipDigest) - .await?; - let tip_digest: Digest = tip_digest.as_tip_digest(); - let tip_header = self - .block_index_db - .get(BlockIndexKey::Block(tip_digest)) - .await - .map(|x| x.as_block_record().block_header) - .expect("Indicated block must exist in block record"); - - let parent = self - .get_block(tip_header.prev_block_digest) + self.get_block(self.tip_block.header().prev_block_digest) .await - .expect("Fetching indicated block must succeed"); - - Some(parent.expect("Indicated block must exist")) + .expect("Fetching indicated block must succeed") } pub async fn get_block_header(&self, block_digest: Digest) -> Option { @@ -470,7 +589,11 @@ impl ArchivalState { }; // Fetch block from disk - let block = self.get_block_from_block_record(record).await?; + let block = Self::get_block_from_block_record( + Self::block_file_path(&self.data_dir, &record), + record, + ) + .await?; Ok(Some(block)) } @@ -839,6 +962,44 @@ impl ArchivalState { Ok(()) } + + pub fn data_dir(&self) -> &DataDirectory { + &self.data_dir + } +} + +impl BlockchainBlockSelector for ArchivalState { + // doc'ed in trait + fn tip_digest(&self) -> Digest { + self.tip_block.hash() + } + + // doc'ed in trait + fn tip_height(&self) -> BlockHeight { + self.tip_block.header().height + } + + // doc'ed in trait + fn genesis_digest(&self) -> Digest { + self.genesis_block.hash() + } + + // doc'ed in trait + async fn height_to_canonical_digest(&self, h: BlockHeight) -> Option { + self.block_height_to_canonical_block_digest(h, self.tip_digest()) + .await + } + + // doc'ed in trait + async fn digest_to_canonical_height(&self, d: Digest) -> Option { + match self + .block_belongs_to_canonical_chain(d, self.tip_digest()) + .await + { + true => self.get_block_header(d).await.map(|h| h.height), + false => None, + } + } } #[cfg(test)] @@ -889,7 +1050,7 @@ mod archival_state_tests { // Ensure that the archival state can be initialized without overflowing the stack let seed: [u8; 32] = thread_rng().gen(); let mut rng: StdRng = SeedableRng::from_seed(seed); - let network = Network::RegTest; + let network = Network::Regtest; let mut archival_state0 = make_test_archival_state(network).await; @@ -916,7 +1077,7 @@ mod archival_state_tests { #[tokio::test] async fn archival_state_init_test() -> Result<()> { // Verify that archival mutator set is populated with outputs from genesis block - let network = Network::RegTest; + let network = Network::Regtest; let archival_state = make_test_archival_state(network).await; assert_eq!( @@ -1128,7 +1289,7 @@ mod archival_state_tests { // blocks. let mut rng = thread_rng(); - let network = Network::RegTest; + let network = Network::Regtest; let (mut archival_state, _peer_db_lock, _data_dir) = mock_genesis_archival_state(network).await; let genesis_wallet_state = @@ -1138,7 +1299,7 @@ mod archival_state_tests { .nth_generation_spending_key_for_tests(0) .to_address(); let mut global_state_lock = - mock_genesis_global_state(Network::RegTest, 42, genesis_wallet).await; + mock_genesis_global_state(Network::Regtest, 42, genesis_wallet).await; let mut num_utxos = Block::premine_utxos(network).len(); // 1. Create new block 1 with one input and four outputs and store it to disk @@ -1251,7 +1412,7 @@ mod archival_state_tests { // This test is intended to verify that rollbacks work for non-trivial // blocks, also when there are many blocks that push the active window of the // mutator set forwards. - let network = Network::RegTest; + let network = Network::Regtest; let genesis_wallet_state = mock_genesis_wallet_state(WalletSecret::devnet_wallet(), network).await; let genesis_wallet = genesis_wallet_state.wallet_secret; @@ -1259,7 +1420,7 @@ mod archival_state_tests { .nth_generation_spending_key_for_tests(0) .to_address(); let mut global_state_lock = - mock_genesis_global_state(Network::RegTest, 42, genesis_wallet).await; + mock_genesis_global_state(Network::Regtest, 42, genesis_wallet).await; let mut global_state = global_state_lock.lock_guard_mut().await; let genesis_block: Block = *global_state.chain.archival_state().genesis_block.to_owned(); @@ -1420,7 +1581,7 @@ mod archival_state_tests { #[tokio::test] async fn allow_consumption_of_genesis_output_test() -> Result<()> { let mut rng = thread_rng(); - let network = Network::RegTest; + let network = Network::Regtest; let genesis_wallet_state = mock_genesis_wallet_state(WalletSecret::devnet_wallet(), network).await; let genesis_wallet = genesis_wallet_state.wallet_secret; @@ -1478,7 +1639,7 @@ mod archival_state_tests { // Test various parts of the state update when a block contains multiple inputs and outputs let mut rng = thread_rng(); - let network = Network::RegTest; + let network = Network::Regtest; let genesis_wallet_state = mock_genesis_wallet_state(WalletSecret::devnet_wallet(), network).await; let genesis_spending_key = genesis_wallet_state @@ -1507,7 +1668,7 @@ mod archival_state_tests { ); // Send two outputs each to Alice and Bob, from genesis receiver - let fee = NeptuneCoins::one(); + let fee = NeptuneCoins::one_nau(); let sender_randomness: Digest = random(); let tx_outputs_for_alice = vec![ TxOutput::fake_address( @@ -1870,7 +2031,7 @@ mod archival_state_tests { .await, "AMS must be correctly updated" ); - assert_eq!(block_2, state.chain.archival_state().get_tip().await); + assert_eq!(block_2, *state.chain.archival_state().tip()); assert_eq!( block_1, state.chain.archival_state().get_tip_parent().await.unwrap() @@ -1887,19 +2048,16 @@ mod archival_state_tests { Network::Alpha, Network::Beta, Network::Main, - Network::RegTest, + Network::Regtest, Network::Testnet, ] { let mut archival_state: ArchivalState = make_test_archival_state(network).await; assert!( - archival_state.get_tip_from_disk().await.unwrap().is_none(), + archival_state.get_tip_from_disk().await.is_none(), "Must return None when no block is stored in DB" ); - assert_eq!( - archival_state.genesis_block(), - &archival_state.get_tip().await - ); + assert_eq!(archival_state.genesis_block(), archival_state.tip()); assert!( archival_state.get_tip_parent().await.is_none(), "Genesis tip has no parent" @@ -1920,13 +2078,13 @@ mod archival_state_tests { assert_eq!( mock_block_1, - archival_state.get_tip_from_disk().await.unwrap().unwrap(), + *archival_state.get_tip_from_disk().await.unwrap(), "Returned block must match the one inserted" ); - assert_eq!(mock_block_1, archival_state.get_tip().await); + assert_eq!(mock_block_1, *archival_state.tip()); assert_eq!( - archival_state.genesis_block(), - &archival_state.get_tip_parent().await.unwrap() + Some(archival_state.genesis_block()), + archival_state.get_tip_parent().await.as_ref() ); // Add a 2nd block and verify that this new block is now returned @@ -1939,17 +2097,17 @@ mod archival_state_tests { add_block_to_archival_state(&mut archival_state, mock_block_2.clone()) .await .unwrap(); - let ret2 = archival_state.get_tip_from_disk().await.unwrap(); + let ret2 = archival_state.get_tip_from_disk().await; assert!( ret2.is_some(), "Must return a block when one is stored to DB" ); assert_eq!( mock_block_2, - ret2.unwrap(), + *ret2.unwrap(), "Returned block must match the one inserted" ); - assert_eq!(mock_block_2, archival_state.get_tip().await); + assert_eq!(mock_block_2, *archival_state.tip()); assert_eq!(mock_block_1, archival_state.get_tip_parent().await.unwrap()); } @@ -2839,6 +2997,7 @@ mod archival_state_tests { .as_tip_digest(); assert_eq!(mock_block_1.hash(), tip_digest); + assert_eq!(archival_state.tip_digest(), tip_digest); // Verify that `Block` is stored correctly let actual_block: BlockRecord = archival_state @@ -2960,14 +3119,16 @@ mod archival_state_tests { ); // Test `get_latest_block_from_disk` - let read_latest_block = archival_state.get_tip_from_disk().await?.unwrap(); + let read_latest_block = *archival_state.get_tip_from_disk().await.unwrap(); assert_eq!(mock_block_2, read_latest_block); // Test `get_block_from_block_record` - let block_from_block_record = archival_state - .get_block_from_block_record(actual_block_record_2) - .await - .unwrap(); + let block_from_block_record = ArchivalState::get_block_from_block_record( + ArchivalState::block_file_path(&archival_state.data_dir, &actual_block_record_2), + actual_block_record_2, + ) + .await + .unwrap(); assert_eq!(mock_block_2, block_from_block_record); assert_eq!(mock_block_2.hash(), block_from_block_record.hash()); @@ -2980,17 +3141,17 @@ mod archival_state_tests { // Test `get_block_header` { - let block_header_2_from_lock_method = archival_state + let block_header_2_method = archival_state .get_block_header(mock_block_2.hash()) .await .unwrap(); - assert_eq!(mock_block_2.kernel.header, block_header_2_from_lock_method); + assert_eq!(mock_block_2.kernel.header, block_header_2_method); - let genesis_header_from_lock_method = archival_state + let genesis_header_method = archival_state .get_block_header(genesis.hash()) .await .unwrap(); - assert_eq!(genesis.kernel.header, genesis_header_from_lock_method); + assert_eq!(genesis.kernel.header, genesis_header_method); } // Test `block_height_to_block_headers` diff --git a/src/models/state/blockchain_state.rs b/src/models/state/blockchain_state.rs index 46378df6..f69cfc36 100644 --- a/src/models/state/blockchain_state.rs +++ b/src/models/state/blockchain_state.rs @@ -1,6 +1,11 @@ +use tasm_lib::Digest; + use super::archival_state::ArchivalState; use super::light_state::LightState; +use crate::models::blockchain::block::block_height::BlockHeight; +use crate::models::blockchain::block::traits::BlockchainBlockSelector; + /// `BlockChainState` provides an `Archival` variant /// for full nodes and a `Light` variant for light nodes. /// @@ -13,11 +18,10 @@ use super::light_state::LightState; /// // silence possible clippy bug / false positive. // see: https://github.com/rust-lang/rust-clippy/issues/9798 -#[allow(clippy::large_enum_variant)] #[derive(Debug)] pub enum BlockchainState { /// represents a Archival blockchain state - Archival(BlockchainArchivalState), + Archival(ArchivalState), /// represents Light node blockchain state (ie the current tip) Light(LightState), } @@ -35,18 +39,7 @@ impl BlockchainState { #[inline] pub fn archival_state(&self) -> &ArchivalState { match self { - Self::Archival(bac) => &bac.archival_state, - Self::Light(_) => panic!("archival_state not available in LightState mode"), - } - } - - /// retrieve blockchain archival state. - /// - /// panics if called by a light node. - #[inline] - pub fn blockchain_archival_state(&self) -> &BlockchainArchivalState { - match self { - Self::Archival(bac) => bac, + Self::Archival(a) => a, Self::Light(_) => panic!("archival_state not available in LightState mode"), } } @@ -57,7 +50,7 @@ impl BlockchainState { #[inline] pub fn archival_state_mut(&mut self) -> &mut ArchivalState { match self { - Self::Archival(bac) => &mut bac.archival_state, + Self::Archival(a) => a, Self::Light(_) => panic!("archival_state not available in LightState mode"), } } @@ -66,8 +59,8 @@ impl BlockchainState { #[inline] pub fn light_state(&self) -> &LightState { match self { - Self::Archival(bac) => &bac.light_state, - Self::Light(light_state) => light_state, + Self::Archival(a) => a.tip(), + Self::Light(l) => l, } } @@ -75,20 +68,56 @@ impl BlockchainState { #[inline] pub fn light_state_mut(&mut self) -> &mut LightState { match self { - Self::Archival(bac) => &mut bac.light_state, - Self::Light(light_state) => light_state, + Self::Archival(a) => a.tip_mut(), + Self::Light(l) => l, } } } -/// The `BlockchainArchivalState` contains database access to block headers. -/// -/// It is divided into `ArchivalState` and `LightState`. -#[derive(Debug)] -pub struct BlockchainArchivalState { - /// Historical blockchain data, persisted - pub archival_state: ArchivalState, +impl BlockchainBlockSelector for BlockchainState { + // doc'ed in trait + fn tip_digest(&self) -> Digest { + self.light_state().hash() + } - /// The present tip. - pub light_state: LightState, + // doc'ed in trait + fn tip_height(&self) -> BlockHeight { + self.light_state().header().height + } + + // doc'ed in trait + // + // panics for light-state + // Probably LightState should be modified to hold the genesis block + // the way that ArchivalState does. + fn genesis_digest(&self) -> Digest { + match self { + Self::Archival(a) => a.genesis_digest(), + Self::Light(_) => todo!(), + } + } + + // doc'ed in trait + // + // panics for light-state as it does not have any history + // the only way it could impl this would be to query peer(s) + // or some decentralized data-storage layer. + async fn height_to_canonical_digest(&self, h: BlockHeight) -> Option { + match self { + Self::Archival(a) => a.height_to_canonical_digest(h).await, + Self::Light(_) => unimplemented!(), + } + } + + // doc'ed in trait + // + // panics for light-state as it does not have any history + // the only way it could impl this would be to query peer(s) + // or some decentralized data-storage layer. + async fn digest_to_canonical_height(&self, d: Digest) -> Option { + match self { + Self::Archival(a) => a.digest_to_canonical_height(d).await, + Self::Light(_) => unimplemented!(), + } + } } diff --git a/src/models/state/mempool.rs b/src/models/state/mempool.rs index fe5d24cf..3b623ead 100644 --- a/src/models/state/mempool.rs +++ b/src/models/state/mempool.rs @@ -522,7 +522,7 @@ mod tests { #[tokio::test] async fn get_densest_transactions() { // Verify that transactions are returned ordered by fee density, with highest fee density first - let mempool = setup(10, Network::RegTest).await; + let mempool = setup(10, Network::Regtest).await; let max_fee_density: FeeDensity = FeeDensity::new(BigInt::from(u128::MAX), BigInt::from(1)); let mut prev_fee_density = max_fee_density; @@ -617,7 +617,7 @@ mod tests { let mut rng: StdRng = SeedableRng::from_seed(seed); - let network = Network::RegTest; + let network = Network::Regtest; let devnet_wallet = WalletSecret::devnet_wallet(); let mut premine_receiver_global_state = mock_genesis_global_state(network, 2, devnet_wallet).await; @@ -829,7 +829,7 @@ mod tests { // First put a transaction into the mempool. Then mine block 1a does // not contain this transaction, such that mempool is still non-empty. // Then mine a a block 1b that also does not contain this transaction. - let network = Network::RegTest; + let network = Network::Regtest; let devnet_wallet = WalletSecret::devnet_wallet(); let mut premine_receiver_global_state = mock_genesis_global_state(network, 2, devnet_wallet).await; @@ -963,7 +963,7 @@ mod tests { #[tokio::test] async fn conflicting_txs_preserve_highest_fee() -> Result<()> { // Create a global state object, controlled by a preminer who receives a premine-UTXO. - let network = Network::RegTest; + let network = Network::Regtest; let mut preminer_state_lock = mock_genesis_global_state(network, 2, WalletSecret::devnet_wallet()).await; let now = Block::genesis_block(network).kernel.header.timestamp; diff --git a/src/models/state/mod.rs b/src/models/state/mod.rs index 52d78ab7..1207b881 100644 --- a/src/models/state/mod.rs +++ b/src/models/state/mod.rs @@ -10,6 +10,7 @@ use std::cmp::max; use std::ops::Deref; use std::ops::DerefMut; +use anyhow::anyhow; use anyhow::bail; use anyhow::Result; use blockchain_state::BlockchainState; @@ -17,14 +18,18 @@ use itertools::Itertools; use mempool::Mempool; use networking_state::NetworkingState; use num_traits::CheckedSub; +use serde::Deserialize; +use serde::Serialize; use tracing::debug; use tracing::info; use tracing::warn; use twenty_first::math::digest::Digest; use twenty_first::util_types::algebraic_hasher::AlgebraicHasher; +use wallet::address::KeyType; use wallet::address::ReceivingAddress; use wallet::address::SpendingKey; use wallet::expected_utxo::UtxoNotifier; +use wallet::utxo_transfer::UtxoTransferEncrypted; use wallet::wallet_state::WalletState; use wallet::wallet_status::WalletStatus; @@ -33,7 +38,10 @@ 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::blockchain::block::block_selector::BlockSelector; +use crate::models::blockchain::transaction::AnnouncedUtxo; +use crate::models::blockchain::transaction::OwnedUtxoNotifyMethod; +use crate::models::blockchain::transaction::UnownedUtxoNotifyMethod; use crate::models::peer::HandshakeData; use crate::models::state::wallet::expected_utxo::ExpectedUtxo; use crate::models::state::wallet::monitored_utxo::MonitoredUtxo; @@ -48,12 +56,13 @@ use super::blockchain::block::Block; use super::blockchain::transaction::primitive_witness::PrimitiveWitness; use super::blockchain::transaction::primitive_witness::SaltedUtxos; use super::blockchain::transaction::transaction_kernel::TransactionKernel; -use super::blockchain::transaction::utxo::Utxo; use super::blockchain::transaction::validity::TransactionValidationLogic; use super::blockchain::transaction::Transaction; +use super::blockchain::transaction::TxAddressOutput; use super::blockchain::transaction::TxInputList; use super::blockchain::transaction::TxOutput; use super::blockchain::transaction::TxOutputList; +use super::blockchain::transaction::TxParams; use super::blockchain::type_scripts::native_currency::NativeCurrency; use super::blockchain::type_scripts::neptune_coins::NeptuneCoins; use super::blockchain::type_scripts::time_lock::TimeLock; @@ -61,14 +70,6 @@ use super::blockchain::type_scripts::TypeScript; use super::consensus::tasm::program::ConsensusProgram; use super::consensus::timestamp::Timestamp; -#[derive(Debug, Clone)] -struct TransactionDetails { - pub tx_inputs: TxInputList, - pub tx_outputs: TxOutputList, - pub fee: NeptuneCoins, - pub timestamp: Timestamp, -} - /// `GlobalStateLock` holds a [`tokio::AtomicRw`](crate::locks::tokio::AtomicRw) /// ([`RwLock`](std::sync::RwLock)) over [`GlobalState`]. /// @@ -195,6 +196,11 @@ impl GlobalStateLock { self.lock_guard_mut().await.resync_membership_proofs().await } + /// retrieve wallet status data for tip block + pub async fn get_wallet_status_for_tip(&self) -> WalletStatus { + self.lock_guard().await.get_wallet_status_for_tip().await + } + pub async fn prune_abandoned_monitored_utxos( &mut self, block_depth_threshhold: usize, @@ -216,6 +222,241 @@ impl GlobalStateLock { self.lock_guard_mut().await.cli = cli.clone(); self.cli = cli; } + + /// Generate tx params for use by `create_transaction()` and `send()` + /// + /// This method simplifies building [TxParams]. It should be used + /// when the following are true: + /// * each output is sent to a `ReceivingAddress` + /// * each output sends a non-zero amount of native coins. + /// * all unowned outputs can have the same notification policy + /// * all owned outputs can have the same notification policy + /// * a change output should be created + /// * the change spending key can be a `SymmetricKey` + /// * no non-native coins or custom lockscripts are used. + /// + /// Otherwise, the [TxParams] must be generated some other way. + /// + /// params: + /// + outputs: a list of (ReceivingAddress,amount) where amount is > 0. + /// + fee: mining fee. (must be >= 0) + /// + owned_utxo_notify_method: notification mechanism for self-owned outputs, including change. + /// + unowned_utxo_notify_method: notification mechanism for 3rd party outputs + /// + /// returns: + /// + [TxParams]: transaction parameters for send() and create_transaction() + /// + Vec<[TxOutputMeta]>: list of output metadata, one per output in TxParams::tx_output_list(). + /// + /// The output metadata is provided to enable the caller to match [TxOutput] in tx_output_list + /// with the [ReceivingAddress] that were provided as input, as well as an automatically generated + /// change address. It is guaranteed that the length and order of the output metadata is the + /// same as TxOutput::tx_output_list(), so the two can be zip'ed together. + pub async fn generate_tx_params( + &mut self, + outputs: Vec, + fee: NeptuneCoins, + owned_utxo_notify_method: OwnedUtxoNotifyMethod, + unowned_utxo_notify_method: UnownedUtxoNotifyMethod, + ) -> Result<(TxParams, Vec)> { + // obtain next unused symmetric key for change utxo + let change_key = { + let mut s = self.lock_guard_mut().await; + let key = s.wallet_state.next_unused_spending_key(KeyType::Symmetric); + + // write state to disk. create_transaction() may be slow. + s.persist_wallet().await.expect("flushed"); + key + }; + + self.lock_guard() + .await + .generate_tx_params( + outputs, + change_key, + fee, + owned_utxo_notify_method, + unowned_utxo_notify_method, + Timestamp::now(), + ) + .await + } + + /// Send coins to 1 or more recipients + /// + /// `tx_params` contains inputs and outputs, typically created + /// by [Self::generate_tx_params]. + /// + /// returns: a [Transaction] upon success, else [None]. + pub async fn send(&mut self, tx_params: TxParams) -> Result { + let tx_output_list = tx_params.tx_output_list().clone(); + + // Create the transaction + // + // Note that create_transaction() does not modify any state and only + // requires acquiring a read-lock which does not block other tasks. + // This is important because internally it calls prove() which is a very + // lengthy operation. + // + // note: A change output will be added to tx_outputs if needed. + let transaction = self + .lock_guard() + .await + .create_transaction(tx_params) + .await?; + + // acquire write-lock + let mut gsm = self.lock_guard_mut().await; + + // insert transaction into mempool + if gsm.mempool.insert(&transaction).is_some() { + bail!("the transaction attempts to spend inputs already spent by another transaction in the mempool with a higher fee. try increasing the fee."); + } + + // if the tx created offchain expected_utxos we must inform wallet. + if tx_output_list.has_offchain() { + // Inform wallet of any expected incoming utxos. + // note that this (briefly) mutates self. + gsm.add_expected_utxos_to_wallet(tx_output_list.expected_utxos_iter()) + .await?; + + // ensure we write new wallet state out to disk. + gsm.persist_wallet().await.expect("flushed wallet"); + } + + Ok(transaction) + } + + /// claim a utxo + /// + /// The input string must be a valid bech32m encoded `UtxoTransferEncrypted` + /// for the current network and the wallet must have the corresponding + /// `SpendingKey` for decryption. + /// + /// upon success, a new `ExpectedUtxo` will be added to the local wallet + /// state. + /// + /// if the utxo has already been claimed, an error will result. + pub async fn claim_utxo(&mut self, utxo_transfer_encrypted_str: String) -> Result<()> { + // deserialize UtxoTransferEncrypted from bech32m string. + let utxo_transfer_encrypted = + UtxoTransferEncrypted::from_bech32m(&utxo_transfer_encrypted_str, self.cli().network)?; + + // acquire global state read lock + let state = self.lock_guard().await; + + // find known spending key by receiver_identifier + let spending_key = state + .wallet_state + .find_known_spending_key_for_receiver_identifier( + utxo_transfer_encrypted.receiver_identifier, + ) + .ok_or(anyhow!("utxo does not match any known wallet key"))?; + + // decrypt utxo_transfer_encrypted into UtxoTransfer + let utxo_transfer = utxo_transfer_encrypted.decrypt_with_spending_key(&spending_key)?; + + tracing::debug!("claim-utxo: decrypted {:#?}", utxo_transfer); + + // search for matching monitored utxo and return early if found. + if state + .wallet_state + .find_monitored_utxo(&utxo_transfer.utxo) + .await + .is_some() + { + info!("found monitored utxo. returning early."); + return Ok(()); + } + + // construct an AnnouncedUtxo + let announced_utxo = AnnouncedUtxo { + utxo: utxo_transfer.utxo, + sender_randomness: utxo_transfer.sender_randomness, + receiver_preimage: spending_key.privacy_preimage(), + }; + + // check if wallet is already expecting this utxo. + let has_expected_utxo = state + .wallet_state + .has_expected_utxo(&(announced_utxo.clone(), UtxoNotifier::Claim).into()) + .await; + + // look for a canonical block that has this utxo as an output + let maybe_prepared_claim = match state + .chain + .archival_state() + .find_canonical_block_with_output( + announced_utxo.addition_record(), + BlockSelector::Genesis, + ) + .await + { + Some(b) => { + // get a stream for retrieving blocks from parent(b) .. tip. + // perf: fast. this only returns the stream, it doesn't iterate it. + let block_stream = state + .chain + .archival_state() + .canonical_block_stream_asc( + BlockSelector::Digest(b.header().prev_block_digest), + BlockSelector::Tip, + ) + .await; + + // prepare a claim. + // perf: this is potentially lengthy as it iterates over all blocks + // in the stream and also generates utxo membership proofs for each. + let prepared_claim = state + .wallet_state + .prepare_claim_utxo_in_block(announced_utxo.clone(), block_stream) + .await?; + + Some(prepared_claim) + } + None => None, + }; + + // release global state read lock + drop(state); + + // we only acquire write-lock if the utxo is already confirmed + // in a block or the wallet does not have the expected_utxo + if maybe_prepared_claim.is_some() || !has_expected_utxo { + // acquire global state write-lock + let mut gsm = self.lock_guard_mut().await; + + // add expected_utxo to wallet if not existing. + // + // note: we add it even if block is already confirmed, although not + // required for claiming. This is just so that we have it in the + // wallet for consistency and backup. + if !has_expected_utxo { + gsm.add_expected_utxos_to_wallet([ + (announced_utxo.clone(), UtxoNotifier::Claim).into() + ]) + .await?; + }; + + // write prepared claim if utxo was already confirmed in a block. + if let Some(prepared_claim) = maybe_prepared_claim { + gsm.wallet_state + .finalize_claim_utxo_in_block(prepared_claim) + .await?; + } + + // ensure we write new wallet state out to disk. + gsm.persist_wallet().await.expect("flushed wallet"); + } + + Ok(()) + } + + pub async fn next_spending_key(&mut self, key_type: KeyType) -> SpendingKey { + self.lock_guard_mut() + .await + .next_spending_key(key_type) + .await + } } impl Deref for GlobalStateLock { @@ -277,9 +518,7 @@ impl GlobalState { pub async fn get_wallet_status_for_tip(&self) -> WalletStatus { let tip_digest = self.chain.light_state().hash(); - self.wallet_state - .get_wallet_status_from_lock(tip_digest) - .await + self.wallet_state.get_wallet_status(tip_digest).await } pub async fn get_latest_balance_height(&self) -> Option { @@ -410,17 +649,11 @@ impl GlobalState { .map(TypeScript::new) .to_vec(); - let lock_script_witnesses = tx_inputs - .spending_keys_iter() - .into_iter() - .map(|k| k.unlock_key().values().to_vec()) - .collect_vec(); - PrimitiveWitness { input_utxos: SaltedUtxos::new(tx_inputs.utxos()), input_lock_scripts: tx_inputs.lock_scripts(), type_scripts, - lock_script_witnesses, + lock_script_witnesses: tx_inputs.lock_script_witnesses(), input_membership_proofs: tx_inputs.ms_membership_proofs(), output_utxos: SaltedUtxos::new(tx_outputs.utxos()), mutator_set_accumulator, @@ -430,8 +663,8 @@ impl GlobalState { /// generates [TxOutputList] from a list of address:amount pairs (outputs). /// - /// This is a helper method for generating the `TxOutputList` that - /// is required by [Self::create_transaction()] and [Self::create_raw_transaction()]. + /// This is a helper method for generating the `TxOutputList` that is part + /// of [TxParams]. /// /// Each output may use either `OnChain` or `OffChain` notifications. See documentation of /// of [TxOutput::auto()] for a description of the logic and the @@ -443,16 +676,16 @@ impl GlobalState { /// future work: /// /// see future work comment in [TxOutput::auto()] - pub fn generate_tx_outputs( + pub fn generate_tx_outputs<'a>( &self, - outputs: impl IntoIterator, - owned_utxo_notify_method: UtxoNotifyMethod, + outputs: impl Iterator, + owned_utxo_notify_method: OwnedUtxoNotifyMethod, + unowned_utxo_notify_method: UnownedUtxoNotifyMethod, ) -> Result { let block_height = self.chain.light_state().header().height; // Convert outputs. [address:amount] --> TxOutputList let tx_outputs: Vec<_> = outputs - .into_iter() .map(|(address, amount)| { let sender_randomness = self .wallet_state @@ -463,10 +696,11 @@ impl GlobalState { // based on whether the address belongs to our wallet or not TxOutput::auto( &self.wallet_state, - &address, - amount, + address, + *amount, sender_randomness, owned_utxo_notify_method, + unowned_utxo_notify_method, ) }) .collect::>()?; @@ -474,153 +708,115 @@ impl GlobalState { 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 - /// automatically created, such that: - /// - /// change = sum(inputs) - sum(outputs) - fee. - /// - /// When finer control is required, [Self::create_raw_transaction()] - /// can be used instead. + /// Generates TxParams (including change output) from an + /// existing TxOutputList that represents all outputs except for the change + /// output. /// - /// The `tx_outputs` parameter should normally be generated with - /// [Self::generate_tx_outputs()] which determines which outputs should be - /// `OnChain` or `OffChain`. - /// - /// 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. Any [ExpectedUtxo], including change can then be retrieved - /// with [TxOutputList::expected_utxos()]. - /// - /// The `change_utxo_notify_method` parameter should normally be - /// [UtxoNotifyMethod::OnChain] for safest transfer. - /// - /// The change_key should normally be a [SpendingKey::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 - /// calls prove() which is a very lengthy operation. - /// - /// Example: - /// - /// ```compile_fail - /// - /// // 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_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; - /// - /// // 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, // 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. - /// state - /// .lock_guard_mut() - /// .await - /// .add_expected_utxos_to_wallet(tx_outputs.expected_utxos()) - /// .await?; - /// ``` - pub async fn create_transaction( + /// This is useful when ReceivingAddress is not available, such as when + /// creating output Utxo directly from lockscripts or using non-native + /// coins/tokens. Otherwise [Self::generate_tx_params()] is preferred. + pub async fn generate_tx_params_from_tx_outputs( &self, - tx_outputs: &mut TxOutputList, + mut tx_output_list: TxOutputList, change_key: SpendingKey, - change_utxo_notify_method: UtxoNotifyMethod, + change_utxo_notify_method: OwnedUtxoNotifyMethod, fee: NeptuneCoins, timestamp: Timestamp, - ) -> Result { - // 1. create/add change output if necessary. - let total_spend = tx_outputs.total_native_coins() + fee; - + ) -> Result { + let total_spend = tx_output_list.total_native_coins() + fee; let tip_hash = self.chain.light_state().hash(); // collect spendable inputs - let tx_inputs = self + let tx_input_list = self .wallet_state - .allocate_sufficient_input_funds_from_lock(total_spend, tip_hash, timestamp) + .allocate_sufficient_input_funds_at_timestamp(total_spend, tip_hash, timestamp) .await?; - let input_amount = tx_inputs.total_native_coins(); + let input_amount = tx_input_list.total_native_coins(); if total_spend < input_amount { - let block_height = self.chain.light_state().header().height; - - let amount = input_amount.checked_sub(&total_spend).ok_or_else(|| { + let change_amount = input_amount.checked_sub(&total_spend).ok_or_else(|| { anyhow::anyhow!("underflow subtracting total_spend from input_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 change_outputs = [(change_key.to_address(), change_amount)]; + let mut change_output_list = self.generate_tx_outputs( + change_outputs.iter(), + change_utxo_notify_method, + UnownedUtxoNotifyMethod::OnChain, + )?; - 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()) - } - } - }; + assert_eq!(change_output_list.len(), 1); - tx_outputs.push(tx_output); + tx_output_list.append(&mut change_output_list); } - // 2. Create the transaction - let transaction = self - .create_raw_transaction(tx_inputs, tx_outputs.clone(), fee, timestamp) + Ok(TxParams::new(tx_input_list, tx_output_list)?) + } + + /// see [GlobalStateLock::generate_tx_params()] + /// + /// the difference here is that caller must/may supply a change_key. + pub async fn generate_tx_params( + &self, + mut outputs: Vec, + change_key: SpendingKey, + fee: NeptuneCoins, + owned_utxo_notify_method: OwnedUtxoNotifyMethod, + unowned_utxo_notify_method: UnownedUtxoNotifyMethod, + timestamp: Timestamp, + ) -> Result<(TxParams, Vec)> { + let total_spend = outputs + .iter() + .map(|(_, amount)| *amount) + .sum::() + + fee; + let tip_hash = self.chain.light_state().hash(); + + // collect spendable inputs + let tx_input_list = self + .wallet_state + .allocate_sufficient_input_funds_at_timestamp(total_spend, tip_hash, timestamp) .await?; - Ok(transaction) + let input_amount = tx_input_list.total_native_coins(); + + if total_spend < input_amount { + let change_amount = input_amount.checked_sub(&total_spend).ok_or_else(|| { + anyhow::anyhow!("underflow subtracting total_spend from input_amount") + })?; + + outputs.push((change_key.to_address(), change_amount)); + } + + let tx_output_list = self.generate_tx_outputs( + outputs.iter(), + owned_utxo_notify_method, + unowned_utxo_notify_method, + )?; + + assert_eq!(tx_output_list.len(), outputs.len()); + + let tx_output_meta_list = outputs + .into_iter() + .map(|(addr, _)| TxOutputMeta { + self_owned: self + .wallet_state + .find_known_spending_key_for_receiving_address(&addr) + .is_some(), + receiving_address: addr, + }) + .collect_vec(); + + let tx_params = TxParams::new_with_timestamp(tx_input_list, tx_output_list, timestamp)?; + + Ok((tx_params, tx_output_meta_list)) } /// creates a Transaction. /// /// This API provides the caller complete control over selection of inputs - /// and outputs. When fine grained control is not required, - /// [Self::create_transaction()] is easier to use and should be preferred. + /// and outputs. /// /// It is the caller's responsibility to provide inputs and outputs such /// that sum(inputs) == sum(outputs) + fee. Else an error will result. @@ -628,34 +824,87 @@ impl GlobalState { /// 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()] which determines which outputs should be - /// `OnChain` or `OffChain`. + /// The `tx_params` parameter should normally be generated with + /// [Self::generate_tx_params()] which selects inputs and creates change + /// output /// /// After this call returns it is the caller's responsibility to inform the /// wallet of any returned [ExpectedUtxo] 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 + /// Note that `create_transaction()` does not modify any state and does /// not require acquiring write lock. This is important becauce internally /// it calls prove() which is a very lengthy operation. /// /// Example: /// - /// See the implementation of [Self::create_transaction()]. - pub async fn create_raw_transaction( - &self, - tx_inputs: TxInputList, - tx_outputs: TxOutputList, - fee: NeptuneCoins, - timestamp: Timestamp, - ) -> Result { - // UTXO data: inputs, outputs, and supporting witness data - let tx_data = self - .generate_tx_details_for_transaction(tx_inputs, tx_outputs, fee, timestamp) - .await?; + /// ```compile_fail + /// + /// let addr = ReceivingAddress::from(GenerationReceivingAddress::derive_from_seed(rand::random())); + /// let outputs = vec![(addr, NeptuneCoins::new(1))]; + /// + /// // obtain next unused symmetric key for change utxo + /// let change_key = { + /// let mut s = global_state_lock.lock_guard_mut().await; + /// let key = s.wallet_state.next_unused_spending_key(KeyType::Symmetric); + /// + /// // write state to disk. create_transaction() may be slow. + /// s.persist_wallet().await.expect("flushed"); + /// key + /// }; + /// + /// let state = global_state_lock.lock_guard().await; + /// let (tx_params, _) = state + /// .generate_tx_params( + /// outputs, + /// change_key, + /// NeptuneCoins::zero(), // fee, + /// Default::default(), // owned_utxo_notify_method, + /// Default::default(), // unowned_utxo_notify_method, + /// Timestamp::now(), + /// ) + /// .await + /// .map_err(|e| e.to_string()) + /// + /// let transaction = state + /// .create_transaction(tx_params.clone()) + /// .await + /// .map_err(|e| e.to_string())?; + /// + /// drop(state); + /// + /// // write any off-chain notifications to disk + /// if tx_params.tx_output_list.has_offchain() { + /// // acquire write-lock + /// let mut gsm = state_state_lock.lock_guard_mut().await; + /// + /// // Inform wallet of any expected incoming utxos. + /// // note that this (briefly) mutates self. + /// gsm.add_expected_utxos_to_wallet(tx_output_list.expected_utxos_iter()) + /// .await + /// .map_err(|e| e.to_string())?; + /// } + /// ``` + pub async fn create_transaction(&self, tx_params: TxParams) -> Result { + let mutator_set_accumulator = self + .chain + .light_state() + .kernel + .body + .mutator_set_accumulator + .clone(); + let privacy = self.cli().privacy; - self.create_transaction_from_data(tx_data).await + // note: this executes the prover which can take a very + // long time, perhaps minutes. As such, we use + // spawn_blocking() to execute on tokio's blocking + // threadpool and avoid blocking the tokio executor + // and other async tasks. + let transaction = tokio::task::spawn_blocking(move || { + Self::create_transaction_worker(tx_params, mutator_set_accumulator, privacy) + }) + .await?; + Ok(transaction) } /// This is a simple wrapper around create_transaction @@ -667,8 +916,6 @@ impl GlobalState { fee: NeptuneCoins, timestamp: Timestamp, ) -> Result<(Transaction, Vec)> { - let mut tx_outputs = TxOutputList::from(tx_output_vec); - // note: should use next_unused_generation_spending_key() // but that requires &mut self. let change_key = self @@ -676,82 +923,19 @@ impl GlobalState { .wallet_secret .nth_symmetric_key_for_tests(0); - let len = tx_outputs.len(); - let transaction = self - .create_transaction( - &mut tx_outputs, + let tx_params = self + .generate_tx_params_from_tx_outputs( + tx_output_vec.into(), change_key.into(), - UtxoNotifyMethod::OffChain, + OwnedUtxoNotifyMethod::OffChain, fee, timestamp, ) .await?; - info!("receivers len before: {len}, after: {}", tx_outputs.len()); - Ok((transaction, (&tx_outputs).into())) - } - - /// Given a list of UTXOs with receiver data, assemble owned and synced and spendable - /// UTXOs that unlock enough funds, add (and track) a change UTXO if necessary, and - /// and produce a list of removal records, input UTXOs (with lock scripts and - /// membership proofs), addition records, and output UTXOs. - async fn generate_tx_details_for_transaction( - &self, - tx_inputs: TxInputList, - tx_outputs: TxOutputList, - fee: NeptuneCoins, - timestamp: Timestamp, - ) -> Result { - // total amount to be spent -- determines how many and which UTXOs to use - let total_spend: NeptuneCoins = tx_outputs.total_native_coins() + fee; - let input_amount = tx_inputs.total_native_coins(); - - // sanity check: do we even have enough funds? - if total_spend > input_amount { - debug!("Insufficient funds. total_spend: {total_spend}, input_amount: {input_amount}"); - bail!("Not enough available funds."); - } - if total_spend < input_amount { - let diff = total_spend - input_amount; - bail!("Missing change output in the amount of {}", diff); - } - - Ok(TransactionDetails { - tx_inputs, - tx_outputs, - fee, - timestamp, - }) - } - /// Assembles a transaction kernel and supporting witness or proof(s) from - /// the given transaction data. - async fn create_transaction_from_data( - &self, - transaction_details: TransactionDetails, - ) -> Result { - let mutator_set_accumulator = self - .chain - .light_state() - .kernel - .body - .mutator_set_accumulator - .clone(); - let privacy = self.cli().privacy; + let transaction = self.create_transaction(tx_params.clone()).await?; - // note: this executes the prover which can take a very - // long time, perhaps minutes. As such, we use - // spawn_blocking() to execute on tokio's blocking - // threadpool and avoid blocking the tokio executor - // and other async tasks. - let transaction = tokio::task::spawn_blocking(move || { - Self::create_transaction_from_data_worker( - transaction_details, - mutator_set_accumulator, - privacy, - ) - }) - .await?; - Ok(transaction) + Ok((transaction, (tx_params.tx_output_list()).into())) } // note: this executes the prover which can take a very @@ -760,33 +944,28 @@ impl GlobalState { // Use create_transaction_from_data() instead. // // fixme: why is _privacy param unused? - fn create_transaction_from_data_worker( - transaction_details: TransactionDetails, + fn create_transaction_worker( + tx_params: TxParams, mutator_set_accumulator: MutatorSetAccumulator, _privacy: bool, ) -> Transaction { - let TransactionDetails { - tx_inputs, - tx_outputs, - fee, - timestamp, - } = transaction_details; - // complete transaction kernel let kernel = TransactionKernel { - inputs: tx_inputs.removal_records(&mutator_set_accumulator), - outputs: tx_outputs.addition_records(), - public_announcements: tx_outputs.public_announcements(), - fee, - timestamp, + inputs: tx_params + .tx_input_list() + .removal_records(&mutator_set_accumulator), + outputs: tx_params.tx_output_list().addition_records(), + public_announcements: tx_params.tx_output_list().public_announcements(), + fee: tx_params.fee(), + timestamp: *tx_params.timestamp(), coinbase: None, mutator_set_hash: mutator_set_accumulator.hash(), }; // populate witness let primitive_witness = Self::generate_primitive_witness( - &tx_inputs, - &tx_outputs, + tx_params.tx_input_list(), + tx_params.tx_output_list(), kernel.clone(), mutator_set_accumulator, ); @@ -814,14 +993,7 @@ impl GlobalState { expected_utxos: impl IntoIterator, ) -> Result<()> { for expected_utxo in expected_utxos.into_iter() { - self.wallet_state - .add_expected_utxo(ExpectedUtxo::new( - expected_utxo.utxo, - expected_utxo.sender_randomness, - expected_utxo.receiver_preimage, - expected_utxo.received_from, - )) - .await; + self.wallet_state.add_expected_utxo(expected_utxo).await; } Ok(()) } @@ -1001,15 +1173,17 @@ impl GlobalState { }; // try latest (block hash, membership proof) entry - let (block_hash, mut membership_proof) = monitored_utxo + let (block_hash, membership_proof_ref) = monitored_utxo .get_latest_membership_proof_entry() .expect("Database not in consistent state. Monitored UTXO must have at least one membership proof."); + let mut membership_proof = membership_proof_ref.to_owned(); + // request path-to-tip let (backwards, _luca, forwards) = self .chain .archival_state() - .find_path(block_hash, tip_hash) + .find_path(*block_hash, tip_hash) .await; // after this point, we may be modifying it. @@ -1220,7 +1394,7 @@ impl GlobalState { self.chain.archival_state_mut().block_index_db.flush().await; // persist archival_mutator_set, with sync label - let hash = self.chain.archival_state().get_tip().await.hash(); + let hash = self.chain.archival_state().tip().hash(); self.chain .archival_state_mut() .archival_mutator_set @@ -1274,6 +1448,16 @@ impl GlobalState { new_block: Block, coinbase_utxo_info: Option, ) -> Result<()> { + // log summary. + info!("Storing block:\n height {}:\n digest: {}\n timestamp: {}\n difficulty: {}\n inputs: {}\n outputs: {}\n", + new_block.header().height, + new_block.hash().to_hex(), + new_block.header().timestamp.standard_format(), + new_block.header().difficulty, + new_block.body().transaction.kernel.inputs.len(), + new_block.body().transaction.kernel.outputs.len(), + ); + // Apply the updates myself .chain @@ -1286,8 +1470,7 @@ impl GlobalState { .chain .archival_state_mut() .update_mutator_set(&new_block) - .await - .expect("Updating mutator set must succeed"); + .await?; if let Some(coinbase_info) = coinbase_utxo_info { // Notify wallet to expect the coinbase UTXO, as we mined this block @@ -1382,6 +1565,25 @@ impl GlobalState { pub fn cli(&self) -> &cli_args::Args { &self.cli } + + pub async fn next_spending_key(&mut self, key_type: KeyType) -> SpendingKey { + let key = self.wallet_state.next_unused_spending_key(key_type); + + // persist wallet state to disk + self.persist_wallet().await.expect("flushed"); + + key + } +} + +/// This provides some additional metadata about `TxOutput` that are generated +/// by GlobalState::generate_tx_params() +/// +/// note that it is possible to have TxOutput with no corresponding ReceivingAddress. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TxOutputMeta { + pub receiving_address: ReceivingAddress, + pub self_owned: bool, } #[cfg(test)] @@ -1399,6 +1601,7 @@ mod global_state_tests { use crate::config_models::network::Network; use crate::models::blockchain::block::Block; + use crate::models::blockchain::transaction::utxo::Utxo; use crate::models::state::wallet::expected_utxo::UtxoNotifier; use crate::tests::shared::add_block_to_light_state; use crate::tests::shared::make_mock_block; @@ -1438,7 +1641,7 @@ mod global_state_tests { #[traced_test] #[tokio::test] async fn premine_recipient_cannot_spend_premine_before_and_can_after_release_date() { - let network = Network::RegTest; + let network = Network::Regtest; let other_wallet = WalletSecret::new_random(); let global_state_lock = mock_genesis_global_state(network, 2, WalletSecret::devnet_wallet()).await; @@ -1578,7 +1781,7 @@ mod global_state_tests { #[tokio::test] async fn restore_monitored_utxos_from_recovery_data_test() { let mut rng = thread_rng(); - let network = Network::RegTest; + let network = Network::Regtest; let devnet_wallet = WalletSecret::devnet_wallet(); let mut global_state_lock = mock_genesis_global_state(network, 2, devnet_wallet).await; let mut global_state = global_state_lock.lock_guard_mut().await; @@ -1655,7 +1858,7 @@ mod global_state_tests { #[tokio::test] async fn resync_ms_membership_proofs_simple_test() -> Result<()> { let mut rng = thread_rng(); - let network = Network::RegTest; + let network = Network::Regtest; let mut global_state_lock = mock_genesis_global_state(network, 2, WalletSecret::devnet_wallet()).await; let mut global_state = global_state_lock.lock_guard_mut().await; @@ -1671,20 +1874,20 @@ mod global_state_tests { let seven_months = Timestamp::months(7); let (mock_block_1a, _, _) = make_mock_block(&genesis_block, None, other_receiver_address, rng.gen()); - { - global_state - .chain - .archival_state_mut() - .write_block_as_tip(&mock_block_1a) - .await?; - } // Verify that wallet has a monitored UTXO (from genesis) let wallet_status = global_state.get_wallet_status_for_tip().await; + assert!(!wallet_status .synced_unspent_available_amount(launch + seven_months) .is_zero()); + global_state + .chain + .archival_state_mut() + .write_block_as_tip(&mock_block_1a) + .await?; + // Verify that this is unsynced with mock_block_1a assert!( global_state @@ -1725,7 +1928,7 @@ mod global_state_tests { #[tokio::test] async fn resync_ms_membership_proofs_fork_test() -> Result<()> { let mut rng = thread_rng(); - let network = Network::RegTest; + let network = Network::Regtest; let mut global_state_lock = mock_genesis_global_state(network, 2, WalletSecret::devnet_wallet()).await; let mut global_state = global_state_lock.lock_guard_mut().await; @@ -1736,7 +1939,7 @@ mod global_state_tests { let own_receiving_address = own_spending_key.to_address(); // 1. Create new block 1a where we receive a coinbase UTXO, store it - let genesis_block = global_state.chain.archival_state().get_tip().await; + let genesis_block = global_state.chain.archival_state().tip().to_owned(); let (mock_block_1a, coinbase_utxo, coinbase_output_randomness) = make_mock_block(&genesis_block, None, own_receiving_address, rng.gen()); global_state @@ -1755,7 +1958,7 @@ mod global_state_tests { // Verify that wallet has monitored UTXOs, from genesis and from block_1a let wallet_status = global_state .wallet_state - .get_wallet_status_from_lock(mock_block_1a.hash()) + .get_wallet_status(mock_block_1a.hash()) .await; assert_eq!(2, wallet_status.synced_unspent.len()); @@ -1781,7 +1984,7 @@ mod global_state_tests { // Verify that one MUTXO is unsynced, and that 1 (from genesis) is synced let wallet_status_after_forking = global_state .wallet_state - .get_wallet_status_from_lock(parent_block.hash()) + .get_wallet_status(parent_block.hash()) .await; assert_eq!(1, wallet_status_after_forking.synced_unspent.len()); assert_eq!(1, wallet_status_after_forking.unsynced_unspent.len()); @@ -1811,7 +2014,7 @@ mod global_state_tests { #[tokio::test] async fn resync_ms_membership_proofs_across_stale_fork() -> Result<()> { let mut rng = thread_rng(); - let network = Network::RegTest; + let network = Network::Regtest; let mut global_state_lock = mock_genesis_global_state(network, 2, WalletSecret::devnet_wallet()).await; let mut global_state = global_state_lock.lock_guard_mut().await; @@ -1824,7 +2027,7 @@ mod global_state_tests { .to_address(); // 1. Create new block 1a where we receive a coinbase UTXO, store it - let genesis_block = global_state.chain.archival_state().get_tip().await; + let genesis_block = global_state.chain.archival_state().tip().to_owned(); assert!(genesis_block.kernel.header.height.is_genesis()); let (mock_block_1a, coinbase_utxo_1a, cb_utxo_output_randomness_1a) = make_mock_block(&genesis_block, None, own_receiving_address, rng.gen()); @@ -1845,7 +2048,7 @@ mod global_state_tests { // Verify that UTXO was recorded let wallet_status_after_1a = global_state .wallet_state - .get_wallet_status_from_lock(mock_block_1a.hash()) + .get_wallet_status(mock_block_1a.hash()) .await; assert_eq!(2, wallet_status_after_1a.synced_unspent.len()); } @@ -1865,7 +2068,7 @@ mod global_state_tests { // Verify that all both MUTXOs have synced MPs let wallet_status_on_a_fork = global_state .wallet_state - .get_wallet_status_from_lock(fork_a_block.hash()) + .get_wallet_status(fork_a_block.hash()) .await; assert_eq!(2, wallet_status_on_a_fork.synced_unspent.len()); @@ -1885,7 +2088,7 @@ mod global_state_tests { // Verify that there are zero MUTXOs with synced MPs let wallet_status_on_b_fork_before_resync = global_state .wallet_state - .get_wallet_status_from_lock(fork_b_block.hash()) + .get_wallet_status(fork_b_block.hash()) .await; assert_eq!( 0, @@ -1903,7 +2106,7 @@ mod global_state_tests { .unwrap(); let wallet_status_on_b_fork_after_resync = global_state .wallet_state - .get_wallet_status_from_lock(fork_b_block.hash()) + .get_wallet_status(fork_b_block.hash()) .await; assert_eq!(2, wallet_status_on_b_fork_after_resync.synced_unspent.len()); assert_eq!( @@ -1928,7 +2131,7 @@ mod global_state_tests { // Verify that there are zero MUTXOs with synced MPs let wallet_status_on_c_fork_before_resync = global_state .wallet_state - .get_wallet_status_from_lock(fork_c_block.hash()) + .get_wallet_status(fork_c_block.hash()) .await; assert_eq!( 0, @@ -1947,7 +2150,7 @@ mod global_state_tests { .unwrap(); let wallet_status_on_c_fork_after_resync = global_state .wallet_state - .get_wallet_status_from_lock(fork_c_block.hash()) + .get_wallet_status(fork_c_block.hash()) .await; assert_eq!(1, wallet_status_on_c_fork_after_resync.synced_unspent.len()); assert_eq!( @@ -1998,7 +2201,7 @@ mod global_state_tests { seed }; let mut rng: StdRng = SeedableRng::from_seed(seed); - let network = Network::RegTest; + let network = Network::Regtest; let genesis_wallet_state = mock_genesis_wallet_state(WalletSecret::devnet_wallet(), network).await; @@ -2028,7 +2231,7 @@ mod global_state_tests { ); // Send two outputs each to Alice and Bob, from genesis receiver - let fee = NeptuneCoins::one(); + let fee = NeptuneCoins::one_nau(); let sender_randomness: Digest = rng.gen(); let tx_outputs_for_alice = vec![ TxOutput::fake_address( @@ -2291,7 +2494,7 @@ mod global_state_tests { #[tokio::test] async fn mock_global_state_is_valid() { let mut rng = thread_rng(); - let network = Network::RegTest; + let network = Network::Regtest; let mut global_state_lock = mock_genesis_global_state(network, 2, WalletSecret::devnet_wallet()).await; let mut global_state = global_state_lock.lock_guard_mut().await; @@ -2326,7 +2529,7 @@ mod global_state_tests { #[tokio::test] #[allow(clippy::needless_return)] async fn onchain_symmetric_change_exists() -> Result<()> { - change_exists(UtxoNotifyMethod::OnChain, KeyType::Symmetric).await + change_exists(OwnedUtxoNotifyMethod::OnChain, KeyType::Symmetric).await } /// test scenario: onchain/generation. @@ -2337,7 +2540,7 @@ mod global_state_tests { #[tokio::test] #[allow(clippy::needless_return)] async fn onchain_generation_change_exists() -> Result<()> { - change_exists(UtxoNotifyMethod::OnChain, KeyType::Generation).await + change_exists(OwnedUtxoNotifyMethod::OnChain, KeyType::Generation).await } /// test scenario: offchain/symmetric. @@ -2348,7 +2551,7 @@ mod global_state_tests { #[tokio::test] #[allow(clippy::needless_return)] async fn offchain_symmetric_change_exists() -> Result<()> { - change_exists(UtxoNotifyMethod::OffChain, KeyType::Symmetric).await + change_exists(OwnedUtxoNotifyMethod::OffChain, KeyType::Symmetric).await } /// test scenario: offchain/generation. @@ -2359,7 +2562,7 @@ mod global_state_tests { #[tokio::test] #[allow(clippy::needless_return)] async fn offchain_generation_change_exists() -> Result<()> { - change_exists(UtxoNotifyMethod::OffChain, KeyType::Generation).await + change_exists(OwnedUtxoNotifyMethod::OffChain, KeyType::Generation).await } /// basic scenario: alice receives 20,000 coins in the premine. 7 months @@ -2409,15 +2612,16 @@ mod global_state_tests { /// 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, + owned_utxo_notify_method: OwnedUtxoNotifyMethod, change_key_type: KeyType, ) -> Result<()> { // setup initial conditions - let network = Network::RegTest; + 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()); + let unowned_utxo_notify_method = UnownedUtxoNotifyMethod::OnChain; // amounts used in alice-to-bob transaction. let alice_to_bob_amount = NeptuneCoins::new(20); @@ -2456,32 +2660,35 @@ mod global_state_tests { .next_unused_spending_key(change_key_type); // create an output for bob, worth 20. + // owned_utxo_notify_method is a test param. 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, + let (tx_params, _) = alice_state_mut + .generate_tx_params( + outputs, alice_change_key, - utxo_notify_method, alice_to_bob_fee, + owned_utxo_notify_method, + unowned_utxo_notify_method, seven_months_post_launch, ) .await?; + let tx_output_list = tx_params.tx_output_list().clone(); + + // create tx. + let alice_to_bob_tx = alice_state_mut.create_transaction(tx_params).await?; + // Inform alice wallet of any expected incoming utxos. - // note: no-op when all utxo notifications are sent on-chain. + // note: no-op when owned utxo notifications are sent on-chain. alice_state_mut - .add_expected_utxos_to_wallet(tx_outputs.expected_utxos_iter()) + .add_expected_utxos_to_wallet(tx_output_list.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) + // add tx to block. (weird this is allowed after block mined) block_1 .accumulate_transaction( alice_to_bob_tx, @@ -2582,9 +2789,11 @@ mod global_state_tests { // 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), + // For offchain-serialized notification, it will be 0. funds may still be claimed. + let alice_expected_balance_by_method = match owned_utxo_notify_method { + OwnedUtxoNotifyMethod::OnChain => NeptuneCoins::new(19979), + OwnedUtxoNotifyMethod::OffChain => NeptuneCoins::new(0), + OwnedUtxoNotifyMethod::OffChainSerialized => NeptuneCoins::new(0), }; // verify that our on/offchain prediction is correct. diff --git a/src/models/state/wallet/address.rs b/src/models/state/wallet/address.rs index a7844213..929750b3 100644 --- a/src/models/state/wallet/address.rs +++ b/src/models/state/wallet/address.rs @@ -1,5 +1,5 @@ mod address_type; -mod common; +pub(super) mod common; pub mod generation_address; pub mod symmetric_key; diff --git a/src/models/state/wallet/address/address_type.rs b/src/models/state/wallet/address/address_type.rs index fb6448c9..c5eb975f 100644 --- a/src/models/state/wallet/address/address_type.rs +++ b/src/models/state/wallet/address/address_type.rs @@ -6,17 +6,13 @@ use serde::Deserialize; use serde::Serialize; use tasm_lib::triton_vm::prelude::Digest; use tracing::warn; -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::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 super::common; @@ -31,14 +27,18 @@ use super::symmetric_key; // actually stored in PublicAnnouncement. /// enumerates available cryptographic key implementations for sending and receiving funds. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, clap::ValueEnum)] #[repr(u8)] pub enum KeyType { - /// [generation_address] built on [twenty_first::math::lattice::kem] + /// private/public keypair. (give public key to 3rd parties) + /// + /// [generation_address] built on [lattice::kem](tasm_lib::prelude::twenty_first::math::lattice::kem) /// /// wraps a symmetric key built on aes-256-gcm Generation = generation_address::GENERATION_FLAG_U8, + /// private key only. (never show to 3rd parties) + /// /// [symmetric_key] built on aes-256-gcm Symmetric = symmetric_key::SYMMETRIC_KEY_FLAG_U8, } @@ -194,29 +194,72 @@ impl ReceivingAddress { /// 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. + /// security: note that if this is used on a symmetric key anyone that can view + /// it will be able to spend the funds. In general it is best practice to avoid + /// display of any part of a symmetric key. 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"), + Self::Symmetric(k) => k.to_bech32m(network), } } - /// parses an address from its bech32m encoding + /// returns human-readable-prefix (hrp) for a given network + pub fn get_hrp(&self, network: Network) -> String { + match self { + Self::Generation(_) => generation_address::GenerationReceivingAddress::get_hrp(network), + Self::Symmetric(_) => symmetric_key::SymmetricKey::get_hrp(network).to_string(), + } + } + + /// returns an abbreviated address. + /// + /// The idea is that this suitable for human recognition purposes + /// + /// ```text + /// format: ... + /// + /// [4 or 6] human readable prefix. 4 for symmetric-key, 6 for generation. + /// 8 start of address. + /// 8 end of address. + /// ``` + /// + /// security: note that if this is used on a symmetric key it will display 16 chars + /// of the bech32m encoded key. This seriously reduces the key's strength and it + /// may be possible to brute-force it. In general it is best practice to avoid + /// display of any part of a symmetric key. /// - /// 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. + /// todo: + /// + /// it would be nice to standardize on a single prefix-len. 6 chars seems a + /// bit much. maybe we could shorten generation prefix to 4 somehow, eg: + /// ngkm --> neptune-generation-key-mainnet + pub fn to_bech32m_abbreviated(&self, network: Network) -> Result { + let bech32 = self.to_bech32m(network)?; + let first_len = self.get_hrp(network).len() + 8usize; + let last_len = 8usize; + + assert!(bech32.len() > first_len + last_len); + + let (first, _) = bech32.split_at(first_len); + let (_, last) = bech32.split_at(bech32.len() - last_len); + + Ok(format!("{}...{}", first, last)) + } + + /// parses an address from its bech32m encoding pub fn from_bech32m(encoded: &str, network: Network) -> Result { - let addr = generation_address::GenerationReceivingAddress::from_bech32m(encoded, network)?; - Ok(addr.into()) + if let Ok(addr) = + generation_address::GenerationReceivingAddress::from_bech32m(encoded, network) + { + return Ok(addr.into()); + } + + let key = symmetric_key::SymmetricKey::from_bech32m(encoded, network)?; + Ok(key.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. @@ -241,7 +284,7 @@ impl ReceivingAddress { /// This enum provides an abstraction API for spending key types, so that a /// method or struct may simply accept a `SpendingKey` and be /// forward-compatible with new types of spending key as they are implemented. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub enum SpendingKey { /// a key from [generation_address] Generation(generation_address::GenerationSpendingKey), @@ -321,7 +364,6 @@ impl SpendingKey { // 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 @@ -348,7 +390,6 @@ impl SpendingKey { // 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, @@ -432,7 +473,6 @@ mod test { } /// 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()); @@ -445,6 +485,10 @@ mod test { } mod worker { + use crate::models::blockchain::shared::Hash; + use crate::util_types::mutator_set::commit; + use tasm_lib::twenty_first::prelude::AlgebraicHasher; + use super::*; /// this tests the generate_public_announcement() and @@ -500,7 +544,7 @@ mod test { // 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!(expected_addition_record, announced_utxo.addition_record()); assert_eq!(sender_randomness, announced_utxo.sender_randomness); assert_eq!(key.privacy_preimage(), announced_utxo.receiver_preimage); } diff --git a/src/models/state/wallet/address/common.rs b/src/models/state/wallet/address/common.rs index 7ad30336..311bcf23 100644 --- a/src/models/state/wallet/address/common.rs +++ b/src/models/state/wallet/address/common.rs @@ -1,3 +1,4 @@ +use crate::config_models::network::Network; use crate::models::blockchain::shared::Hash; use crate::models::blockchain::transaction::utxo::LockScript; use crate::models::blockchain::transaction::PublicAnnouncement; @@ -145,6 +146,15 @@ pub fn lock_script(spending_lock: Digest) -> LockScript { instructions.into() } +/// returns human-readable-prefix for the given network +pub fn network_hrp_char(network: Network) -> char { + match network { + Network::Alpha | Network::Beta | Network::Main => 'm', + Network::Testnet => 't', + Network::Regtest => 'r', + } +} + #[cfg(test)] pub(super) mod test { use super::*; diff --git a/src/models/state/wallet/address/generation_address.rs b/src/models/state/wallet/address/generation_address.rs index 2a6979ca..74500b9c 100644 --- a/src/models/state/wallet/address/generation_address.rs +++ b/src/models/state/wallet/address/generation_address.rs @@ -40,7 +40,7 @@ use twenty_first::util_types::algebraic_hasher::AlgebraicHasher; pub(super) const GENERATION_FLAG_U8: u8 = 79; pub const GENERATION_FLAG: BFieldElement = BFieldElement::new(GENERATION_FLAG_U8 as u64); -#[derive(Clone, Debug, Copy)] +#[derive(Clone, Debug, Copy, Serialize, Deserialize)] pub struct GenerationSpendingKey { pub receiver_identifier: BFieldElement, pub decryption_key: lattice::kem::SecretKey, @@ -49,12 +49,13 @@ pub struct GenerationSpendingKey { pub seed: Digest, } +// 2168 bytes. #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub struct GenerationReceivingAddress { - pub receiver_identifier: BFieldElement, - pub encryption_key: lattice::kem::PublicKey, - pub privacy_digest: Digest, - pub spending_lock: Digest, + pub receiver_identifier: BFieldElement, // 8 bytes + pub encryption_key: lattice::kem::PublicKey, // 2080 bytes + pub privacy_digest: Digest, // 40 bytes + pub spending_lock: Digest, // 40 bytes } impl GenerationSpendingKey { @@ -198,17 +199,10 @@ impl GenerationReceivingAddress { .concat()) } - /// 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(); - let network_byte: char = match network { - Network::Alpha | Network::Beta | Network::Main => 'm', - Network::Testnet => 't', - Network::RegTest => 'r', - }; - hrp.push(network_byte); - hrp + /// returns human readable prefix (hrp) of an address, specific to `network`. + pub fn get_hrp(network: Network) -> String { + // nolga: Neptune lattice-based generation address + format!("nolga{}", common::network_hrp_char(network)) } pub fn to_bech32m(&self, network: Network) -> Result { @@ -228,7 +222,7 @@ impl GenerationReceivingAddress { bail!("Can only decode bech32m addresses."); } - if hrp[0..=5] != Self::get_hrp(network) { + if hrp != Self::get_hrp(network) { bail!("Could not decode bech32m address because of invalid prefix"); } diff --git a/src/models/state/wallet/address/symmetric_key.rs b/src/models/state/wallet/address/symmetric_key.rs index e02842ab..88d1eb11 100644 --- a/src/models/state/wallet/address/symmetric_key.rs +++ b/src/models/state/wallet/address/symmetric_key.rs @@ -1,6 +1,7 @@ //! provides a symmetric key interface based on aes-256-gcm for sending and claiming [Utxo] 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; @@ -10,6 +11,9 @@ use aead::Key; use aead::KeyInit; use aes_gcm::Aes256Gcm; use aes_gcm::Nonce; +use anyhow::bail; +use bech32::FromBase32; +use bech32::ToBase32; use rand::thread_rng; use rand::Rng; use serde::Deserialize; @@ -72,7 +76,7 @@ pub const SYMMETRIC_KEY_FLAG: BFieldElement = BFieldElement::new(SYMMETRIC_KEY_F /// opaque. #[derive(Clone, Debug, Copy, Serialize, Deserialize, PartialEq, Eq)] pub struct SymmetricKey { - seed: Digest, + seed: Digest, // 40 bytes } impl SymmetricKey { @@ -184,4 +188,45 @@ impl SymmetricKey { pub fn lock_script(&self) -> LockScript { common::lock_script(self.spending_lock()) } + + /// encodes the key as bech32m with network-specific prefix + /// + /// security: note that anyone that can view the bech32m string will be able + /// to spend the funds. In general it is best practice to avoid display of + /// any part of a symmetric key. + pub fn to_bech32m(&self, network: Network) -> anyhow::Result { + let hrp = Self::get_hrp(network); + let payload = bincode::serialize(self)?; + let variant = bech32::Variant::Bech32m; + match bech32::encode(&hrp, payload.to_base32(), variant) { + Ok(enc) => Ok(enc), + Err(e) => bail!("Could not encode SymmetricKey as bech32m because error: {e}"), + } + } + + /// decodes a key from bech32m with network-specific prefix + pub fn from_bech32m(encoded: &str, network: Network) -> anyhow::Result { + let (hrp, data, variant) = bech32::decode(encoded)?; + + if variant != bech32::Variant::Bech32m { + bail!("Can only decode bech32m addresses."); + } + + if hrp != *Self::get_hrp(network) { + bail!("Could not decode bech32m address because of invalid prefix"); + } + + let payload = Vec::::from_base32(&data)?; + + match bincode::deserialize(&payload) { + Ok(ra) => Ok(ra), + Err(e) => bail!("Could not decode bech32m because of error: {e}"), + } + } + + /// returns human readable prefix (hrp) of a key, specific to `network` + pub fn get_hrp(network: Network) -> String { + // nsk: neptune-symmetric-key + format!("nsk{}", common::network_hrp_char(network)) + } } diff --git a/src/models/state/wallet/expected_utxo.rs b/src/models/state/wallet/expected_utxo.rs index c14164cc..caa810ee 100644 --- a/src/models/state/wallet/expected_utxo.rs +++ b/src/models/state/wallet/expected_utxo.rs @@ -72,4 +72,5 @@ pub enum UtxoNotifier { Cli, Myself, Premine, + Claim, } diff --git a/src/models/state/wallet/mod.rs b/src/models/state/wallet/mod.rs index 96e8c34e..3242a319 100644 --- a/src/models/state/wallet/mod.rs +++ b/src/models/state/wallet/mod.rs @@ -3,6 +3,7 @@ pub mod coin_with_possible_timelock; pub mod expected_utxo; pub mod monitored_utxo; pub mod rusty_wallet_database; +pub mod utxo_transfer; pub mod wallet_state; pub mod wallet_status; @@ -270,6 +271,62 @@ impl WalletSecret { /// Return the secret key that is used to deterministically generate commitment pseudo-randomness /// for the mutator set. + /// + /// design choices: + /// + /// 1. random or deterministic? + /// + /// This method could generate a random value or a deterministic value. if + /// random, then we don't need to accept any params and can just call + /// rand::random() and we're done. + /// + /// However, there is a [stated goal](https://github.com/Neptune-Crypto/neptune-core/issues/181#issuecomment-2341230087) + /// that if a utxo recipient somehow loses the sender randomness then they + /// could ask the sender to re-send it. The sender could do that either by + /// storing a list of sender_randomness per sent utxo or by regenerating it + /// deterministically. The latter is thought to be simpler. + /// + /// Further choices are constrained by this goal. + /// + /// 2. which params? + /// + /// Ideally each tx output (utxo) would have a unique sender_randomness. + /// We can't really guarantee that deterministically. But we can get + /// pretty close with the params: + /// + /// block_height, receiver_digest, tx_timestamp, output_index + /// + /// But there's a problem. Once a block is mined the tx_timestamp + /// disappears because all the transactions are merged into a single + /// block-tx which has the timestamp of the latest user-tx. So neither + /// recipient nor sender can lookup the user-tx timetamp after the user-tx + /// is confirmed in a block. + /// + /// output_index has the same problem as tx_timestamp. The original index + /// gets lost when all the user-tx are merged into the block-tx. + /// + /// If we remove tx_timestamp and output_index then any user-tx in the same + /// block with same lock_script will share the same sender_randomness. + /// + /// 3. is that a big problem? + /// + /// Apparently not. privacy does not depend on the sender_randomness + /// being unique. + /// + /// So it is decided not to include tx_timestamp and output_index since + /// inclusion would defeat our stated goal. + /// + /// 4. anyone_can_spend, etc + /// + /// Not all output utxos are generated from ReceivingAddress::lock_script(). + /// An example is LockScript::anyone_can_spend(). Unit-test code that uses + /// anyone-can-spend always passes random() for the receiver_digest field + /// which makes the output of sender_randomness non-deterministic anyway. + /// This asymmetry is a bit gross, but does not appear to be an actual + /// problem. + /// + /// further discussion: + /// pub fn generate_sender_randomness( &self, block_height: BlockHeight, @@ -444,7 +501,7 @@ mod wallet_tests { let mut rng = thread_rng(); // This test is designed to verify that the genesis block is applied // to the wallet state at initialization. - let network = Network::RegTest; + let network = Network::Regtest; let mut wallet_state_premine_recipient = mock_genesis_wallet_state(WalletSecret::devnet_wallet(), network).await; let monitored_utxos_premine_wallet = @@ -514,7 +571,7 @@ mod wallet_tests { #[tokio::test] async fn wallet_state_registration_of_monitored_utxos_test() -> Result<()> { let mut rng = thread_rng(); - let network = Network::RegTest; + let network = Network::Regtest; let own_wallet_secret = WalletSecret::new_random(); let mut own_wallet_state = mock_genesis_wallet_state(own_wallet_secret.clone(), network).await; @@ -650,7 +707,7 @@ mod wallet_tests { async fn allocate_sufficient_input_funds_test() -> Result<()> { let mut rng = thread_rng(); let own_wallet_secret = WalletSecret::new_random(); - let network = Network::RegTest; + let network = Network::Regtest; let mut own_wallet_state = mock_genesis_wallet_state(own_wallet_secret, network).await; let own_spending_key = own_wallet_state .wallet_secret @@ -684,7 +741,7 @@ mod wallet_tests { assert_eq!( 1, own_wallet_state - .allocate_sufficient_input_funds(NeptuneCoins::one(), block_1.hash()) + .allocate_sufficient_input_funds(NeptuneCoins::one_nau(), block_1.hash()) .await .unwrap() .len() @@ -693,7 +750,7 @@ mod wallet_tests { 1, own_wallet_state .allocate_sufficient_input_funds( - mining_reward.checked_sub(&NeptuneCoins::one()).unwrap(), + mining_reward.checked_sub(&NeptuneCoins::one_nau()).unwrap(), block_1.hash() ) .await @@ -711,7 +768,10 @@ mod wallet_tests { // Cannot allocate more than we have: `mining_reward` assert!(own_wallet_state - .allocate_sufficient_input_funds(mining_reward + NeptuneCoins::one(), block_1.hash()) + .allocate_sufficient_input_funds( + mining_reward + NeptuneCoins::one_nau(), + block_1.hash() + ) .await .is_err()); @@ -754,7 +814,7 @@ mod wallet_tests { 6, own_wallet_state .allocate_sufficient_input_funds( - mining_reward.scalar_mul(5) + NeptuneCoins::one(), + mining_reward.scalar_mul(5) + NeptuneCoins::one_nau(), next_block.hash() ) .await @@ -775,7 +835,7 @@ mod wallet_tests { // Cannot allocate more than we have: 22 * mining reward assert!(own_wallet_state .allocate_sufficient_input_funds( - expected_balance + NeptuneCoins::one(), + expected_balance + NeptuneCoins::one_nau(), next_block.hash() ) .await @@ -862,7 +922,7 @@ mod wallet_tests { #[tokio::test] async fn wallet_state_maintanence_multiple_inputs_outputs_test() -> Result<()> { let mut rng = thread_rng(); - let network = Network::RegTest; + let network = Network::Regtest; let own_wallet_secret = WalletSecret::new_random(); let mut own_wallet_state = mock_genesis_wallet_state(own_wallet_secret, network).await; let own_spending_key = own_wallet_state @@ -890,6 +950,8 @@ 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 mut now = genesis_block.kernel.header.timestamp; + let tx_timestamp = now + seven_months; let tx_outputs_12_to_other = TxOutput::fake_address( Utxo { @@ -920,12 +982,11 @@ mod wallet_tests { own_address.privacy_digest, ); let tx_outputs_to_other = vec![tx_outputs_12_to_other, tx_outputs_one_to_other]; - let mut now = genesis_block.kernel.header.timestamp; let (valid_tx, expected_utxos) = premine_receiver_global_state .create_transaction_test_wrapper( tx_outputs_to_other.clone(), NeptuneCoins::new(2), - now + seven_months, + tx_timestamp, ) .await .unwrap(); @@ -1058,9 +1119,7 @@ mod wallet_tests { ); // Check that `WalletStatus` is returned correctly - let wallet_status = own_wallet_state - .get_wallet_status_from_lock(block_18.hash()) - .await; + let wallet_status = own_wallet_state.get_wallet_status(block_18.hash()).await; assert_eq!( 19, wallet_status.synced_unspent.len(), diff --git a/src/models/state/wallet/monitored_utxo.rs b/src/models/state/wallet/monitored_utxo.rs index 09c255ae..e31b3ecf 100644 --- a/src/models/state/wallet/monitored_utxo.rs +++ b/src/models/state/wallet/monitored_utxo.rs @@ -72,9 +72,13 @@ impl MonitoredUtxo { } /// Get the most recent (block hash, membership proof) entry in the database, - /// if any. - pub fn get_latest_membership_proof_entry(&self) -> Option<(Digest, MsMembershipProof)> { - self.blockhash_to_membership_proof.iter().next().cloned() + pub fn get_latest_membership_proof_entry(&self) -> Option<&(Digest, MsMembershipProof)> { + self.blockhash_to_membership_proof.iter().next() + } + + /// Get the oldest (block hash, membership proof) entry in the database, + pub fn get_oldest_membership_proof_entry(&self) -> Option<&(Digest, MsMembershipProof)> { + self.blockhash_to_membership_proof.back() } /// Returns true if the MUTXO was abandoned diff --git a/src/models/state/wallet/utxo_transfer.rs b/src/models/state/wallet/utxo_transfer.rs new file mode 100644 index 00000000..d4f76316 --- /dev/null +++ b/src/models/state/wallet/utxo_transfer.rs @@ -0,0 +1,136 @@ +//! This module contains types for transferring [Utxo] notifications between parties +//! outside of neptune-core. (out-of-band) +//! +//! Such transfers look like: +//! +//! 1. sender creates tx params with `rpc.generate_tx_params()` and specifies +//! UnownedUtxoNotifyMethod::OffchainSerialized for at least 1 output utxo. +//! 2. sender sends tx with `rpc.send(tx_params)` +//! 3. sender reads tx_params.tx_output_list().utxo_transfer_iter() to obtain +//! list of `UtxoTransferEncrypted` that must be transferred out-of-band +//! as well as a list of `TxOutputMeta` that has a `ReceivingAddress` for each +//! `UtxoTransferEncrypted` +//! 4. sender serializes the `UtxoTransferEncrypted` into bech32m, possibly +//! along with some metadata. (see neptune-cli source-code for a standard +//! file-format) +//! 5. sender transmits the bech32m encoded `UtxoTransferEncrypted` to the owner +//! of the `ReceivingAddress`. +//! 6. recipient calls rpc.claim_utxo() passing it the received bech32m string. +//! 7. the recipients wallet is now notified of the utxo and applies it towards +//! wallet balance, etc. + +use crate::config_models::network::Network; +use crate::models::blockchain::transaction::utxo::Utxo; +use crate::prelude::twenty_first; +use anyhow::bail; +use anyhow::Result; +use bech32::FromBase32; +use bech32::ToBase32; +use get_size::GetSize; +use serde::{Deserialize, Serialize}; +use tasm_lib::triton_vm::prelude::BFieldElement; +use twenty_first::math::tip5::Digest; + +use super::address::{ReceivingAddress, SpendingKey}; + +/// intended for transferring utxo-notification secrets between parties +/// +/// this type intentionally does not impl Serialize, Deserialize because it +/// should not be transferred directly, but rather encrypted inside +/// UtxoTransferEncrypted +#[derive(Clone, Debug, PartialEq, Eq, Hash, GetSize)] +pub struct UtxoTransfer { + pub utxo: Utxo, + pub sender_randomness: Digest, +} + +impl UtxoTransfer { + /// instantiate + pub fn new(utxo: Utxo, sender_randomness: Digest) -> Self { + Self { + utxo, + sender_randomness, + } + } + + /// encrypts the UtxoTransfer to a [ReceivingAddress] creating a [UtxoTransferEncrypted]. + pub fn encrypt_to_address( + &self, + address: &ReceivingAddress, + ) -> anyhow::Result { + Ok(UtxoTransferEncrypted { + ciphertext: address.encrypt(&self.utxo, self.sender_randomness)?, + receiver_identifier: address.receiver_identifier(), + }) + } +} + +/// an encrypted wrapper for UtxoTransfer. +/// +/// This type is intended to be serialized and actually transferred between +/// parties. +/// +/// note: bech32m encoding of this type is considered standard and is +/// recommended over serde serialization. +/// +/// the receiver_identifier enables the receiver to find the matching +/// `SpendingKey` in their wallet. +#[derive(Clone, Debug, PartialEq, Eq, Hash, GetSize, Serialize, Deserialize)] +pub struct UtxoTransferEncrypted { + /// contains encrypted UtxoTransfer + pub ciphertext: Vec, + + /// enables the receiver to find the matching `SpendingKey` in their wallet. + pub receiver_identifier: BFieldElement, +} + +impl UtxoTransferEncrypted { + /// decrypts into a [UtxoTransfer] + pub fn decrypt_with_spending_key( + &self, + spending_key: &SpendingKey, + ) -> anyhow::Result { + let (utxo, sender_randomness) = spending_key.decrypt(&self.ciphertext)?; + + Ok(UtxoTransfer { + utxo, + sender_randomness, + }) + } + + /// encodes into a bech32m string for the given network + pub fn to_bech32m(&self, network: Network) -> Result { + let hrp = Self::get_hrp(network); + let payload = bincode::serialize(self)?; + let variant = bech32::Variant::Bech32m; + match bech32::encode(&hrp, payload.to_base32(), variant) { + Ok(enc) => Ok(enc), + Err(e) => bail!("Could not encode UtxoTransferEncrypted as bech32m because error: {e}"), + } + } + + /// decodes from a bech32m string and verifies it matches `network` + pub fn from_bech32m(encoded: &str, network: Network) -> Result { + let (hrp, data, variant) = bech32::decode(encoded)?; + + if variant != bech32::Variant::Bech32m { + bail!("Can only decode bech32m addresses."); + } + + if hrp != *Self::get_hrp(network) { + bail!("Could not decode bech32m address because of invalid prefix"); + } + + let payload = Vec::::from_base32(&data)?; + + match bincode::deserialize(&payload) { + Ok(ra) => Ok(ra), + Err(e) => bail!("Could not decode bech32m because of error: {e}"), + } + } + + /// returns human readable prefix (hrp) of a utxo-transfer-encrypted, specific to `network` + pub fn get_hrp(network: Network) -> String { + format!("utxo{}", super::address::common::network_hrp_char(network)) + } +} diff --git a/src/models/state/wallet/wallet_state.rs b/src/models/state/wallet/wallet_state.rs index 9865a5f2..267deeba 100644 --- a/src/models/state/wallet/wallet_state.rs +++ b/src/models/state/wallet/wallet_state.rs @@ -2,13 +2,17 @@ use std::collections::HashMap; use std::error::Error; use std::fmt::Debug; use std::path::PathBuf; +use std::sync::Arc; +use anyhow::anyhow; use anyhow::bail; use anyhow::Result; +use futures::Stream; use itertools::Itertools; use num_traits::Zero; use serde_derive::Deserialize; use serde_derive::Serialize; +use tasm_lib::triton_vm::prelude::BFieldElement; use tokio::fs::OpenOptions; use tokio::io::AsyncBufReadExt; use tokio::io::AsyncWriteExt; @@ -49,6 +53,7 @@ use crate::Hash; use super::address::generation_address; use super::address::symmetric_key; use super::address::KeyType; +use super::address::ReceivingAddress; use super::address::SpendingKey; use super::coin_with_possible_timelock::CoinWithPossibleTimeLock; use super::expected_utxo::ExpectedUtxo; @@ -237,8 +242,29 @@ impl WalletState { wallet_state } - // note: does not verify we do not have any dups. + /// notifies wallet to expect a utxo in a future block. + /// + /// panics if an [ExpectedUtxo] already exists. + /// + /// perf: the dup check is presently o(n). It can be made o(1). see + /// find_expected_utxo() + /// + /// future work: + /// 1) remove panic. return an error instead. + /// + /// 2) at present, if an expected_utxo is somehow added *after* a block is + /// confirmed that contains the utxo, then the wallet will not + /// recognize it. Now that *_claim_utxo_for_block() exist it should be + /// possible to have a maintenance process that checks for any + /// old/unclaimed expected utxos and claims them. + /// + /// likewise add_expected_utxo() could perhaps be refactored to perform + /// a claim() if the target utxo has already been confirmed in a block. pub(crate) async fn add_expected_utxo(&mut self, expected_utxo: ExpectedUtxo) { + if self.has_expected_utxo(&expected_utxo).await { + panic!("ExpectedUtxo already exists in wallet"); + } + self.wallet_db .expected_utxos_mut() .push(expected_utxo) @@ -292,7 +318,7 @@ impl WalletState { // // 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) { + .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); @@ -309,7 +335,7 @@ impl WalletState { /// n = number of ExpectedUtxo in database. (all-time) /// m = number of transaction outputs. /// - /// see https://github.com/Neptune-Crypto/neptune-core/pull/175#issuecomment-2302511025 + /// see /// /// Returns an iterator of [AnnouncedUtxo]. (addition record, UTXO, sender randomness, receiver_preimage) pub async fn scan_for_expected_utxos<'a>( @@ -329,6 +355,54 @@ impl WalletState { .filter_map(move |a| eu_map.get(a).map(|eu| eu.into())) } + /// check if wallet already has the provided `expected_utxo` + /// + /// note that `WalletState::add_expected_utxo()` prevents duplicate + /// [ExpectedUtxo], however its possible for distinct `ExpectedUtxo` to + /// include the same `Utxo`. + /// + /// perf: + /// + /// this fn is o(n) with the number of ExpectedUtxo stored. Iteration is + /// performed from newest to oldest based on expectation that we will most + /// often be working with recent ExpectedUtxos. + /// + /// This fn could be made o(1) if we were to store ExpectedUtxo keyed by + /// hash(ExpectedUtxo). This would require a separate levelDb + /// file for ExpectedUtxo or using a DB such as redb that supports + /// transactional namespaces. + pub async fn has_expected_utxo(&self, expected_utxo: &ExpectedUtxo) -> bool { + let len = self.wallet_db.expected_utxos().len().await; + self.wallet_db + .expected_utxos() + .stream_many_values((0..len).rev()) + .await + .any(|eu| futures::future::ready(eu == *expected_utxo)) + .await + } + + /// find the `MonitoredUtxo` that matches `utxo`, if any + /// + /// perf: this fn is o(n) with the number of MonitoredUtxo stored. Iteration + /// is performed from newest to oldest based on expectation that we + /// will most often be working with recent MonitoredUtxos. + pub async fn find_monitored_utxo(&self, utxo: &Utxo) -> Option { + let len = self.wallet_db.monitored_utxos().len().await; + let stream = self + .wallet_db + .monitored_utxos() + .stream_many_values((0..len).rev()) + .await; + pin_mut!(stream); // needed for iteration + + while let Some(mu) = stream.next().await { + if mu.utxo == *utxo { + return Some(mu); + } + } + None + } + /// Delete all ExpectedUtxo that exceed a certain age /// /// note: It is questionable if this method should ever be called @@ -386,17 +460,39 @@ impl WalletState { // returns true if the utxo can be unlocked by one of the // known wallet keys. pub fn can_unlock(&self, utxo: &Utxo) -> bool { - self.find_spending_key_for_utxo(utxo).is_some() + self.find_known_spending_key_for_utxo(utxo).is_some() } // returns Some(SpendingKey) if the utxo can be unlocked by one of the known // wallet keys. - pub fn find_spending_key_for_utxo(&self, utxo: &Utxo) -> Option { + pub fn find_known_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) } + // returns Some(SpendingKey) if the utxo can be unlocked by one of the known + // wallet keys. + pub fn find_known_spending_key_for_receiving_address( + &self, + addr: &ReceivingAddress, + ) -> Option { + self.get_all_known_spending_keys() + .into_iter() + .find(|k| k.to_address() == *addr) + } + + // returns Some(SpendingKey) if the utxo can be unlocked by one of the known + // wallet keys. + pub fn find_known_spending_key_for_receiver_identifier( + &self, + receiver_identifier: BFieldElement, + ) -> Option { + self.get_all_known_spending_keys() + .into_iter() + .find(|k| k.receiver_identifier() == receiver_identifier) + } + /// 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() @@ -505,7 +601,7 @@ impl WalletState { all_received_outputs .map(|au| { ( - au.addition_record, + au.addition_record(), (au.utxo, au.sender_randomness, au.receiver_preimage), ) }) @@ -817,7 +913,7 @@ impl WalletState { .filter(|(_, eu)| { offchain_received_outputs .iter() - .any(|au| au.addition_record == eu.addition_record) + .any(|au| au.addition_record() == eu.addition_record) }) .map(|(idx, mut eu)| { eu.mined_in_block = Some((new_block.hash(), new_block.kernel.header.timestamp)); @@ -831,6 +927,192 @@ impl WalletState { Ok(()) } + /// prepares utxo claim data but does not modify state. + /// + /// enables claiming Utxo *after* parent Tx is confirmed in a block + /// + /// For claiming *before* the Tx is confirmed, call add_expected_utxo() + /// instead. + /// + /// Claiming can take place at any blockheight after the Tx is confirmed + /// including the confirmation block itself. + /// + /// important: The caller must call finalize_claim_utxo_in_block() with + /// the output of this method in order to modify wallet state. + /// + /// Params: + /// + announced_utxo: details of utxo we are claiming. + /// + `blocks_until_tip` contains a list of canonical blocks where + /// a) the first block is the block before the Tx was confirmed. + /// b) the last block is the tip. + /// + /// perf: + /// + /// `blocks_until_tip` is an async Stream which makes it compatible with + /// ArchivalState::canonical_block_stream_asc() without need to + /// collect/store blocks in RAM (eg a Vec). + /// + /// claim_utxo_for_block has been split into a prepare method (&self) and a + /// finalize (&mut self) method. + /// + /// This prepare method is potentially quite lengthy as it must load and + /// iterate over an unknown number of blocks and it generates a utxo + /// membership proof for each block. The proof generation is performed in + /// tokio's blocking threadpool with spawn_blocking(). + /// + /// As this method is lengthy it must not take &mut self, which would + /// require a global write-lock, blocking all other tasks. + pub(crate) async fn prepare_claim_utxo_in_block( + &self, + announced_utxo: AnnouncedUtxo, + blocks_until_tip: impl Stream>, + ) -> Result<(MonitoredUtxo, IncomingUtxoRecoveryData)> { + pin_mut!(blocks_until_tip); + + // The utxo membership proof is always generated with the MutatorSetAccumulator + // from the *previous* block. So we must have both prev_block and block + // when iterating. + // + // note: we use Arc on the block to avoid cloning the msa (or worse, the + // block) when calling spawn_blocking. also here and in the loop. + let mut loop_prev_block = blocks_until_tip + .next() + .await + .map(Arc::new) + .ok_or_else(|| anyhow!("missing parent of confirmation block"))?; + + // This is the confirmation block. We keep a reference to it for use + // outside the proving loop. + let confirmation_block = Arc::new( + blocks_until_tip + .next() + .await + .ok_or_else(|| anyhow!("missing confirmation block"))?, + ); + + // If output UTXO belongs to us, add it to the list of monitored UTXOs and + // add its membership proof to the list of managed membership proofs. + let AnnouncedUtxo { + utxo, + sender_randomness, + receiver_preimage, + .. + } = announced_utxo; + info!( + "claim_utxo_in_block: Received UTXO in block {}, height {}: value = {}", + confirmation_block.hash(), + confirmation_block.kernel.header.height, + utxo.get_native_currency_amount(), + ); + let utxo_digest = Hash::hash(&utxo); + + let mut mutxo = MonitoredUtxo::new(utxo.clone(), self.number_of_mps_per_utxo); + + // we use chain() to create a stream where the first element is the + // confirmation block. This is needed because we already advanced the + // iterator above. + // + // note: block.clone() just bumps rc refcount. + let mut block_stream = futures::stream::iter([confirmation_block.clone()].into_iter()) + .chain(blocks_until_tip.map(Arc::new)); + + // loop through blocks from confirmation block until tip and + // 1. generate membership proofs + // 2. mark mutxo as spent if that has somehow happened. + while let Some(loop_block) = block_stream.next().await { + let loop_block_ref = loop_prev_block.clone(); // clone bumps arc refcount. + + // 1. generate membership proof + // we use spawn-blocking around prove so it does not block this task + let membership_proof = tokio::task::spawn_blocking(move || { + loop_block_ref.body().mutator_set_accumulator.prove( + utxo_digest, + sender_randomness, + receiver_preimage, + ) + }) + .await?; + + // 2. check if mutxo has been spent (if not already spent) + if mutxo.spent_in_block.is_none() { + let abs_i = membership_proof.compute_indices(Hash::hash(&mutxo.utxo)); + + // if any input absolute-index matches ours, then this utxo has been spent. + if loop_block + .body() + .transaction + .kernel + .inputs + .iter() + .any(|rr| rr.absolute_indices == abs_i) + { + mutxo.spent_in_block = Some(( + loop_block.hash(), + loop_block.kernel.header.timestamp, + loop_block.kernel.header.height, + )); + } + } + + // 3. add membership proof + mutxo.add_membership_proof_for_tip(loop_block.hash(), membership_proof); + + loop_prev_block = loop_block; + } + + // the 0-index entry corresponds to the input block. + assert!(!mutxo.blockhash_to_membership_proof.is_empty()); + assert_eq!( + mutxo.get_oldest_membership_proof_entry().unwrap().0, + confirmation_block.hash() + ); + let aocl_index = mutxo.blockhash_to_membership_proof[0] + .1 + .auth_path_aocl + .leaf_index; + + // Add the new UTXO to the list of monitored UTXOs + mutxo.confirmed_in_block = Some(( + confirmation_block.hash(), + confirmation_block.kernel.header.timestamp, + confirmation_block.kernel.header.height, + )); + + // Add the data required to restore the UTXOs membership proof from public + // data to the secret's file. + let recovery_data = IncomingUtxoRecoveryData { + utxo, + sender_randomness, + receiver_preimage, + aocl_index, + }; + + Ok((mutxo, recovery_data)) + } + + /// writes prepared utxo claim data to disk + /// + /// Informs wallet of a Utxo *after* parent Tx is confirmed in a block + /// + /// The `claim_data` must first be generated with + /// [prepare_claim_utxo_in_block()]. + /// + /// no validation. assumes input data is valid/correct. + /// + /// The caller should persist wallet DB to disk after this returns. + pub(crate) async fn finalize_claim_utxo_in_block( + &mut self, + claim_data: (MonitoredUtxo, IncomingUtxoRecoveryData), + ) -> Result<()> { + let (mutxo, recovery_data) = claim_data; + + // add monitored_utxo + self.wallet_db.monitored_utxos_mut().push(mutxo).await; + + // write to disk. + self.store_utxo_ms_recovery_data(recovery_data).await + } + pub async fn is_synced_to(&self, tip_hash: Digest) -> bool { let db_sync_digest = self.wallet_db.get_sync_label().await; if db_sync_digest != tip_hash { @@ -849,7 +1131,7 @@ impl WalletState { .await } - pub async fn get_wallet_status_from_lock(&self, tip_digest: Digest) -> WalletStatus { + pub async fn get_wallet_status(&self, tip_digest: Digest) -> WalletStatus { let monitored_utxos = self.wallet_db.monitored_utxos(); let mut synced_unspent = vec![]; let mut unsynced_unspent = vec![]; @@ -895,7 +1177,7 @@ impl WalletState { } } - pub async fn allocate_sufficient_input_funds_from_lock( + pub async fn allocate_sufficient_input_funds_at_timestamp( &self, requested_amount: NeptuneCoins, tip_digest: Digest, @@ -903,15 +1185,15 @@ impl WalletState { ) -> Result { // We only attempt to generate a transaction using those UTXOs that have up-to-date // membership proofs. - let wallet_status = self.get_wallet_status_from_lock(tip_digest).await; + let wallet_status = self.get_wallet_status(tip_digest).await; // First check that we have enough. Otherwise return an error. if wallet_status.synced_unspent_available_amount(timestamp) < requested_amount { bail!( - "Insufficient synced amount to create transaction. Requested: {}, Total synced UTXOs: {}. Total synced amount: {}. Synced unspent available amount: {}. Synced unspent timelocked amount: {}. Total unsynced UTXOs: {}. Unsynced unspent amount: {}. Block is: {}", + "Insufficient synced amount to create transaction.\n Requested: {}\n synced UTXOs: {}\n synced unspent amount: {}\n synced unspent available amount: {}\n Synced unspent timelocked amount: {}\n unsynced UTXOs: {}\n unsynced unspent amount: {}\n block is: {}", requested_amount, wallet_status.synced_unspent.len(), - wallet_status.synced_unspent.iter().map(|(wse, _msmp)| wse.utxo.get_native_currency_amount()).sum::(), + wallet_status.synced_unspent_amount(), wallet_status.synced_unspent_available_amount(timestamp), wallet_status.synced_unspent_timelocked_amount(timestamp), wallet_status.unsynced_unspent.len(), @@ -927,16 +1209,17 @@ impl WalletState { wallet_status.synced_unspent[ret.len()].clone(); // find spending key for this utxo. - let spending_key = match self.find_spending_key_for_utxo(&wallet_status_element.utxo) { - Some(k) => k, - None => { - warn!( - "spending key not found for utxo: {:?}", - wallet_status_element.utxo - ); - continue; - } - }; + let spending_key = + match self.find_known_spending_key_for_utxo(&wallet_status_element.utxo) { + Some(k) => k, + None => { + warn!( + "spending key not found for utxo: {:?}", + wallet_status_element.utxo + ); + continue; + } + }; let lock_script = spending_key.to_address().lock_script(); allocated_amount = @@ -945,7 +1228,7 @@ impl WalletState { utxo: wallet_status_element.utxo, lock_script: lock_script.clone(), ms_membership_proof: membership_proof, - spending_key, + unlock_key: spending_key.unlock_key(), }); } @@ -961,7 +1244,7 @@ impl WalletState { tip_digest: Digest, ) -> Result { let now = Timestamp::now(); - self.allocate_sufficient_input_funds_from_lock(requested_amount, tip_digest, now) + self.allocate_sufficient_input_funds_at_timestamp(requested_amount, tip_digest, now) .await } @@ -1024,7 +1307,7 @@ mod tests { // Verify that MUTXO *is* marked as abandoned let mut rng = thread_rng(); - let network = Network::RegTest; + let network = Network::Regtest; let own_wallet_secret = WalletSecret::new_random(); let own_spending_key = own_wallet_secret.nth_generation_spending_key_for_tests(0); let mut own_global_state_lock = @@ -1250,7 +1533,7 @@ mod tests { #[traced_test] #[tokio::test] async fn mock_wallet_state_is_synchronized_to_genesis_block() { - let network = Network::RegTest; + let network = Network::Regtest; let wallet = WalletSecret::devnet_wallet(); let genesis_block = Block::genesis_block(network); @@ -1299,7 +1582,7 @@ mod tests { #[tokio::test] async fn insert_and_scan() { let mut wallet = - mock_genesis_wallet_state(WalletSecret::new_random(), Network::RegTest).await; + mock_genesis_wallet_state(WalletSecret::new_random(), Network::Regtest).await; assert!(wallet.wallet_db.expected_utxos().is_empty().await); assert!(wallet.wallet_db.expected_utxos().len().await.is_zero()); @@ -1352,7 +1635,7 @@ mod tests { #[tokio::test] async fn prune_stale() { let mut wallet = - mock_genesis_wallet_state(WalletSecret::new_random(), Network::RegTest).await; + mock_genesis_wallet_state(WalletSecret::new_random(), Network::Regtest).await; let mock_utxo = Utxo::new_native_coin(LockScript::anyone_can_spend(), NeptuneCoins::new(14)); @@ -1447,7 +1730,7 @@ mod tests { /// the wallet db is NOT persisted to disk after the ExpectedUtxo /// is added. asserts that the restored wallet has 0 ExpectedUtxo. pub(super) async fn restore_wallet(persist: bool) { - let network = Network::RegTest; + let network = Network::Regtest; let wallet_secret = WalletSecret::new_random(); let data_dir = unit_test_data_directory(network).unwrap(); diff --git a/src/models/state/wallet/wallet_status.rs b/src/models/state/wallet/wallet_status.rs index 4c09bdb9..afc13d1e 100644 --- a/src/models/state/wallet/wallet_status.rs +++ b/src/models/state/wallet/wallet_status.rs @@ -40,6 +40,15 @@ pub struct WalletStatus { } impl WalletStatus { + /// returns native currency sum of unspent utxo that have been confirmed in a block + pub fn synced_unspent_amount(&self) -> NeptuneCoins { + self.synced_unspent + .iter() + .map(|(wse, _)| wse.utxo.get_native_currency_amount()) + .sum::() + } + + /// returns native currency sum of unspent utxo that have been confirmed in a block and are available to spend at timestamp pub fn synced_unspent_available_amount(&self, timestamp: Timestamp) -> NeptuneCoins { self.synced_unspent .iter() @@ -48,6 +57,8 @@ impl WalletStatus { .map(|utxo| utxo.get_native_currency_amount()) .sum::() } + + /// returns native currency sum of unspent utxo that have been confirmed in a block and are timelocked but otherwise available to spend at timestamp pub fn synced_unspent_timelocked_amount(&self, timestamp: Timestamp) -> NeptuneCoins { self.synced_unspent .iter() @@ -56,18 +67,24 @@ impl WalletStatus { .map(|utxo| utxo.get_native_currency_amount()) .sum::() } + + /// returns native currency sum of unspent utxo that have not been confirmed in a block pub fn unsynced_unspent_amount(&self) -> NeptuneCoins { self.unsynced_unspent .iter() .map(|wse| wse.utxo.get_native_currency_amount()) .sum::() } + + /// returns native currency sum of spent utxo that have been confirmed in a block pub fn synced_spent_amount(&self) -> NeptuneCoins { self.synced_spent .iter() .map(|wse| wse.utxo.get_native_currency_amount()) .sum::() } + + /// returns native currency sum of spent utxo that have not been confirmed in a block pub fn unsynced_spent_amount(&self) -> NeptuneCoins { self.unsynced_spent .iter() diff --git a/src/peer_loop.rs b/src/peer_loop.rs index a7a4a0d3..1585855a 100644 --- a/src/peer_loop.rs +++ b/src/peer_loop.rs @@ -1305,8 +1305,8 @@ mod peer_loop_tests { .await .chain .archival_state() - .get_tip() - .await; + .tip() + .to_owned(); let mut nonce = different_genesis_block.kernel.header.nonce; nonce[2].increment(); different_genesis_block.set_header_nonce(nonce); @@ -1384,13 +1384,13 @@ 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 peer_address = get_dummy_socket_address(0); - let genesis_block: Block = state_lock + let genesis_block = state_lock .lock_guard() .await .chain .archival_state() - .get_tip() - .await; + .tip() + .to_owned(); // Make a with hash above what the implied threshold from // `target_difficulty` requires @@ -1485,14 +1485,14 @@ mod peer_loop_tests { ) = get_test_genesis_setup(network, 0).await?; let mut global_state_mut = state_lock.lock_guard_mut().await; let peer_address = get_dummy_socket_address(0); - let genesis_block: Block = global_state_mut.chain.archival_state().get_tip().await; + let genesis_block = global_state_mut.chain.archival_state().tip(); let a_wallet_secret = WalletSecret::new_random(); 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()); + make_mock_block_with_valid_pow(genesis_block, None, a_recipient_address, rng.gen()); global_state_mut.set_new_tip(block_1.clone()).await?; drop(global_state_mut); @@ -1549,7 +1549,7 @@ mod peer_loop_tests { let (_peer_broadcast_tx, from_main_rx_clone, to_main_tx, _to_main_rx1, mut state_lock, hsd) = get_test_genesis_setup(network, 0).await?; let mut global_state_mut = state_lock.lock_guard_mut().await; - let genesis_block: Block = global_state_mut.chain.archival_state().get_tip().await; + let genesis_block = global_state_mut.chain.archival_state().tip().to_owned(); let peer_address = get_dummy_socket_address(0); let a_wallet_secret = WalletSecret::new_random(); let a_recipient_address = a_wallet_secret @@ -1641,7 +1641,7 @@ mod peer_loop_tests { let (_peer_broadcast_tx, from_main_rx_clone, to_main_tx, _to_main_rx1, mut state_lock, hsd) = get_test_genesis_setup(network, 0).await?; let mut global_state_mut = state_lock.lock_guard_mut().await; - let genesis_block: Block = global_state_mut.chain.archival_state().get_tip().await; + let genesis_block = global_state_mut.chain.archival_state().tip().to_owned(); let peer_address = get_dummy_socket_address(0); let a_wallet_secret = WalletSecret::new_random(); let a_recipient_address = a_wallet_secret @@ -1707,14 +1707,14 @@ mod peer_loop_tests { let (_peer_broadcast_tx, from_main_rx_clone, to_main_tx, _to_main_rx1, mut state_lock, hsd) = get_test_genesis_setup(network, 0).await?; let mut global_state_mut = state_lock.lock_guard_mut().await; - let genesis_block: Block = global_state_mut.chain.archival_state().get_tip().await; + let genesis_block = global_state_mut.chain.archival_state().tip(); 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_for_tests(0) .to_address(); let (block_1, _, _) = - make_mock_block_with_valid_pow(&genesis_block, None, a_recipient_address, rng.gen()); + make_mock_block_with_valid_pow(genesis_block, None, a_recipient_address, rng.gen()); let (block_2_a, _, _) = make_mock_block_with_valid_pow(&block_1, None, a_recipient_address, rng.gen()); let (block_3_a, _, _) = @@ -1761,7 +1761,7 @@ mod peer_loop_tests { #[traced_test] #[tokio::test] async fn test_peer_loop_receival_of_first_block() -> Result<()> { - let network = Network::RegTest; + let network = Network::Regtest; let mut rng = thread_rng(); // Scenario: client only knows genesis block. Then receives block 1. let (_peer_broadcast_tx, from_main_rx_clone, to_main_tx, mut to_main_rx1, state_lock, hsd) = @@ -1771,13 +1771,13 @@ mod peer_loop_tests { .nth_generation_spending_key_for_tests(0) .to_address(); let peer_address = get_dummy_socket_address(0); - let genesis_block: Block = state_lock + let genesis_block = state_lock .lock_guard() .await .chain .archival_state() - .get_tip() - .await; + .tip() + .to_owned(); let (mock_block_1, _, _) = make_mock_block_with_valid_pow(&genesis_block, None, a_recipient_address, rng.gen()); @@ -1828,17 +1828,17 @@ mod peer_loop_tests { let mut rng = thread_rng(); // In this scenario, the client only knows the genesis block (block 0) and then // receives block 2, meaning that block 1 will have to be requested. - let network = Network::RegTest; + let network = Network::Regtest; 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 peer_address = get_dummy_socket_address(0); - let genesis_block: Block = state_lock + let genesis_block = state_lock .lock_guard() .await .chain .archival_state() - .get_tip() - .await; + .tip() + .to_owned(); let a_wallet_secret = WalletSecret::new_random(); let a_recipient_address = a_wallet_secret .nth_generation_spending_key_for_tests(0) @@ -1900,7 +1900,7 @@ mod peer_loop_tests { #[tokio::test] async fn prevent_ram_exhaustion_test() -> Result<()> { let mut rng = thread_rng(); - let network = Network::RegTest; + let network = Network::Regtest; // In this scenario the peer sends more blocks than the client allows to store in the // fork-reconciliation field. This should result in abandonment of the fork-reconciliation // process as the alternative is that the program will crash because it runs out of RAM. @@ -1921,7 +1921,7 @@ mod peer_loop_tests { let mut global_state_mut = state_lock.lock_guard_mut().await; let (hsd1, peer_address1) = get_dummy_peer_connection_data_genesis(Network::Alpha, 1).await; - let genesis_block: Block = global_state_mut.chain.archival_state().get_tip().await; + let genesis_block = global_state_mut.chain.archival_state().tip(); let own_recipient_address = global_state_mut .wallet_state .wallet_secret @@ -2011,7 +2011,7 @@ mod peer_loop_tests { let mut rng = thread_rng(); // In this scenario, the client know the genesis block (block 0) and block 1, it // then receives block 4, meaning that block 3 and 2 will have to be requested. - let network = Network::RegTest; + let network = Network::Regtest; let ( _peer_broadcast_tx, from_main_rx_clone, @@ -2022,7 +2022,7 @@ mod peer_loop_tests { ) = get_test_genesis_setup(network, 0).await?; let mut global_state_mut = state_lock.lock_guard_mut().await; let peer_address: SocketAddr = get_dummy_socket_address(0); - let genesis_block: Block = global_state_mut.chain.archival_state().get_tip().await; + let genesis_block = global_state_mut.chain.archival_state().tip(); let a_wallet_secret = WalletSecret::new_random(); let a_recipient_address = a_wallet_secret .nth_generation_spending_key_for_tests(0) @@ -2101,13 +2101,13 @@ mod peer_loop_tests { let mut rng = thread_rng(); // In this scenario, the client only knows the genesis block (block 0) and then // receives block 3, meaning that block 2 and 1 will have to be requested. - let network = Network::RegTest; + let network = Network::Regtest; 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 global_state = state_lock.lock_guard().await; let peer_address = get_dummy_socket_address(0); - let genesis_block: Block = global_state.chain.archival_state().get_tip().await; + let genesis_block = global_state.chain.archival_state().tip(); let a_wallet_secret = WalletSecret::new_random(); let a_recipient_address = a_wallet_secret .nth_generation_spending_key_for_tests(0) @@ -2185,7 +2185,7 @@ mod peer_loop_tests { // then receives block 4, meaning that block 3, 2, and 1 will have to be requested. // But the requests are interrupted by the peer sending another message: a new block // notification. - let network = Network::RegTest; + let network = Network::Regtest; let ( _peer_broadcast_tx, from_main_rx_clone, @@ -2200,7 +2200,7 @@ mod peer_loop_tests { .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 genesis_block = global_state_mut.chain.archival_state().tip(); let (block_1, _, _) = make_mock_block_with_valid_pow( &genesis_block.clone(), None, @@ -2295,7 +2295,7 @@ mod peer_loop_tests { // for a list of peers. let mut rng = thread_rng(); - let network = Network::RegTest; + let network = Network::Regtest; let ( _peer_broadcast_tx, from_main_rx_clone, @@ -2312,7 +2312,7 @@ mod peer_loop_tests { .into_values() .collect::>(); - let genesis_block: Block = global_state_mut.chain.archival_state().get_tip().await; + let genesis_block = global_state_mut.chain.archival_state().tip(); let a_wallet_secret = WalletSecret::new_random(); let a_recipient_address = a_wallet_secret .nth_generation_spending_key_for_tests(0) diff --git a/src/rpc_server.rs b/src/rpc_server.rs index 158afcbf..bad0692a 100644 --- a/src/rpc_server.rs +++ b/src/rpc_server.rs @@ -17,19 +17,24 @@ use serde::Serialize; use systemstat::Platform; use systemstat::System; use tarpc::context; -use tokio::sync::mpsc::error::SendError; use tracing::error; use tracing::info; use twenty_first::math::digest::Digest; use twenty_first::util_types::algebraic_hasher::AlgebraicHasher; +use crate::config_models::data_directory::DataDirectory; use crate::config_models::network::Network; use crate::models::blockchain::block::block_header::BlockHeader; use crate::models::blockchain::block::block_height::BlockHeight; use crate::models::blockchain::block::block_info::BlockInfo; use crate::models::blockchain::block::block_selector::BlockSelector; +use crate::models::blockchain::block::traits::BlockchainBlockSelector; use crate::models::blockchain::shared::Hash; -use crate::models::blockchain::transaction::UtxoNotifyMethod; +use crate::models::blockchain::transaction::OwnedUtxoNotifyMethod; +use crate::models::blockchain::transaction::TxAddressOutput; +use crate::models::blockchain::transaction::TxOutputList; +use crate::models::blockchain::transaction::TxParams; +use crate::models::blockchain::transaction::UnownedUtxoNotifyMethod; use crate::models::blockchain::type_scripts::neptune_coins::NeptuneCoins; use crate::models::channel::RPCServerToMain; use crate::models::consensus::timestamp::Timestamp; @@ -38,9 +43,11 @@ use crate::models::peer::PeerInfo; use crate::models::peer::PeerStanding; use crate::models::state::wallet::address::KeyType; use crate::models::state::wallet::address::ReceivingAddress; +use crate::models::state::wallet::address::SpendingKey; use crate::models::state::wallet::coin_with_possible_timelock::CoinWithPossibleTimeLock; use crate::models::state::wallet::wallet_status::WalletStatus; use crate::models::state::GlobalStateLock; +use crate::models::state::TxOutputMeta; use crate::prelude::twenty_first; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -74,6 +81,9 @@ pub trait RPC { // Return which network the client is running async fn network() -> Network; + /// returns information about where neptune-core data is kept + async fn data_directory() -> DataDirectory; + async fn own_listen_address_for_peers() -> Option; /// Return the node's instance-ID which is a globally unique random generated number @@ -112,7 +122,7 @@ pub trait RPC { /// Return the block header for the specified block async fn header(block_selector: BlockSelector) -> Option; - /// Get sum of unspent UTXOs. + /// retrieve confirmed balance async fn synced_balance() -> NeptuneCoins; /// Get the client's wallet transaction history @@ -145,6 +155,27 @@ pub trait RPC { /// Generate a report of all owned and unspent coins, whether time-locked or not. async fn list_own_coins() -> Vec; + /// Generate tx params, for use by send(). + /// + /// for standard payments involving native neptune coins + async fn generate_tx_params( + outputs: Vec, + fee: NeptuneCoins, + owned_utxo_notify_method: OwnedUtxoNotifyMethod, + unowned_utxo_notify_method: UnownedUtxoNotifyMethod, + ) -> Result<(TxParams, Vec), String>; + + /// Generate tx params for use by send. + /// + /// for non-standard payments such as those involving + /// tokens or custom lockscripts. + async fn generate_tx_params_from_tx_outputs( + tx_output_list: TxOutputList, + change_key: SpendingKey, + change_utxo_notify_method: OwnedUtxoNotifyMethod, + fee: NeptuneCoins, + ) -> Result; + /******** CHANGE THINGS ********/ // Place all things that change state here @@ -154,50 +185,17 @@ pub trait RPC { /// Clears standing for ip, whether connected or not 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: 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 - /// [ReceivingAddress]. - /// - /// `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 - /// [ReceivingAddress::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]. + /// See [GlobalStateLock::send()] /// /// todo: shouldn't we return `Transaction` instead? + async fn send(tx_params: TxParams) -> Result; + + /// claim a utxo /// - /// 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<(ReceivingAddress, NeptuneCoins)>, - owned_utxo_notify_method: UtxoNotifyMethod, - fee: NeptuneCoins, - ) -> Option; + /// See [GlobalStateLock::claim_utxo()] + async fn claim_utxo(utxo_transfer_encrypted: String) -> Result<(), String>; /// Stop miner if running async fn pause_miner(); @@ -224,6 +222,9 @@ pub struct NeptuneRPCServer { impl NeptuneRPCServer { async fn confirmations_internal(&self) -> Option { + let span = tracing::debug_span!("rpc::confirmations_internal"); + let _enter = span.enter(); + let state = self.state.lock_guard().await; match state.get_latest_balance_height().await { @@ -245,6 +246,9 @@ impl NeptuneRPCServer { /// Return temperature of CPU, if available. fn cpu_temp_inner() -> Option { + let span = tracing::debug_span!("rpc::cpu_temp_inner"); + let _enter = span.enter(); + let current_system = System::new(); match current_system.cpu_temp() { Ok(temp) => Some(temp), @@ -256,11 +260,31 @@ impl NeptuneRPCServer { impl RPC for NeptuneRPCServer { // documented in trait. do not add doc-comment. async fn network(self, _: context::Context) -> Network { + let span = tracing::debug_span!("rpc::network"); + let _enter = span.enter(); + self.state.cli().network } + // documented in trait. do not add doc-comment. + async fn data_directory(self, _: context::Context) -> DataDirectory { + let span = tracing::debug_span!("rpc::data_directory"); + let _enter = span.enter(); + + self.state + .lock_guard() + .await + .chain + .archival_state() + .data_dir() + .to_owned() + } + // documented in trait. do not add doc-comment. async fn own_listen_address_for_peers(self, _context: context::Context) -> Option { + let span = tracing::debug_span!("rpc::own_listen_address_for_peers"); + let _enter = span.enter(); + let listen_for_peers_ip = self.state.cli().listen_addr; let listen_for_peers_socket = self.state.cli().peer_port; let socket_address = SocketAddr::new(listen_for_peers_ip, listen_for_peers_socket); @@ -269,11 +293,17 @@ impl RPC for NeptuneRPCServer { // documented in trait. do not add doc-comment. async fn own_instance_id(self, _context: context::Context) -> InstanceId { + let span = tracing::debug_span!("rpc::own_instance_id"); + let _enter = span.enter(); + self.state.lock_guard().await.net.instance_id } // documented in trait. do not add doc-comment. async fn block_height(self, _: context::Context) -> BlockHeight { + let span = tracing::debug_span!("rpc::block_height"); + let _enter = span.enter(); + self.state .lock_guard() .await @@ -286,11 +316,17 @@ impl RPC for NeptuneRPCServer { // documented in trait. do not add doc-comment. async fn confirmations(self, _: context::Context) -> Option { + let span = tracing::debug_span!("rpc::confirmations"); + let _enter = span.enter(); + self.confirmations_internal().await } // documented in trait. do not add doc-comment. async fn utxo_digest(self, _: context::Context, leaf_index: u64) -> Option { + let span = tracing::debug_span!("rpc::utxo_digest"); + let _enter = span.enter(); + let state = self.state.lock_guard().await; let aocl = &state.chain.archival_state().archival_mutator_set.ams().aocl; @@ -306,9 +342,12 @@ impl RPC for NeptuneRPCServer { _: context::Context, block_selector: BlockSelector, ) -> Option { + let span = tracing::debug_span!("rpc::block_digest"); + let _enter = span.enter(); + let state = self.state.lock_guard().await; let archival_state = state.chain.archival_state(); - let digest = block_selector.as_digest(&state).await?; + let digest = block_selector.as_digest(archival_state).await?; // verify the block actually exists archival_state .get_block_header(digest) @@ -322,20 +361,30 @@ impl RPC for NeptuneRPCServer { _: context::Context, block_selector: BlockSelector, ) -> Option { + let span = tracing::debug_span!("rpc::block_info"); + let _enter = span.enter(); + let state = self.state.lock_guard().await; - let digest = block_selector.as_digest(&state).await?; - let archival_state = state.chain.archival_state(); + let digest = block_selector.as_digest(&state.chain).await?; - let block = archival_state.get_block(digest).await.unwrap()?; + let block = state + .chain + .archival_state() + .get_block(digest) + .await + .unwrap()?; Some(BlockInfo::from_block_and_digests( &block, - archival_state.genesis_block().hash(), - state.chain.light_state().hash(), + state.chain.genesis_digest(), + state.chain.tip_digest(), )) } // documented in trait. do not add doc-comment. async fn latest_tip_digests(self, _context: tarpc::context::Context, n: usize) -> Vec { + let span = tracing::debug_span!("rpc::latest_tip_digests"); + let _enter = span.enter(); + let state = self.state.lock_guard().await; let latest_block_digest = state.chain.light_state().hash(); @@ -349,6 +398,9 @@ impl RPC for NeptuneRPCServer { // documented in trait. do not add doc-comment. async fn peer_info(self, _: context::Context) -> Vec { + let span = tracing::debug_span!("rpc::peer_info"); + let _enter = span.enter(); + self.state .lock_guard() .await @@ -364,6 +416,9 @@ impl RPC for NeptuneRPCServer { self, _context: tarpc::context::Context, ) -> HashMap { + let span = tracing::debug_span!("rpc::all_sanctioned_peers"); + let _enter = span.enter(); + let mut sanctions_in_memory = HashMap::default(); let global_state = self.state.lock_guard().await; @@ -394,6 +449,9 @@ impl RPC for NeptuneRPCServer { address_string: String, network: Network, ) -> Option { + let span = tracing::debug_span!("rpc::validate_address"); + let _enter = span.enter(); + let ret = if let Ok(address) = ReceivingAddress::from_bech32m(&address_string, network) { Some(address) } else { @@ -412,6 +470,9 @@ impl RPC for NeptuneRPCServer { _ctx: context::Context, amount_string: String, ) -> Option { + let span = tracing::debug_span!("rpc::validate_amount"); + let _enter = span.enter(); + // parse string let amount = if let Ok(amt) = NeptuneCoins::from_str(&amount_string) { amt @@ -425,6 +486,9 @@ impl RPC for NeptuneRPCServer { // documented in trait. do not add doc-comment. async fn amount_leq_synced_balance(self, _ctx: context::Context, amount: NeptuneCoins) -> bool { + let span = tracing::debug_span!("rpc::amount_leq_synced_balance"); + let _enter = span.enter(); + let now = Timestamp::now(); // test inequality let wallet_status = self @@ -438,6 +502,9 @@ impl RPC for NeptuneRPCServer { // documented in trait. do not add doc-comment. async fn synced_balance(self, _context: tarpc::context::Context) -> NeptuneCoins { + let span = tracing::debug_span!("rpc::synced_balance"); + let _enter = span.enter(); + let now = Timestamp::now(); let wallet_status = self .state @@ -450,6 +517,9 @@ impl RPC for NeptuneRPCServer { // documented in trait. do not add doc-comment. async fn wallet_status(self, _context: tarpc::context::Context) -> WalletStatus { + let span = tracing::debug_span!("rpc::wallet_status"); + let _enter = span.enter(); + self.state .lock_guard() .await @@ -463,45 +533,40 @@ impl RPC for NeptuneRPCServer { _context: tarpc::context::Context, block_selector: BlockSelector, ) -> Option { + let span = tracing::debug_span!("rpc::header"); + let _enter = span.enter(); + let state = self.state.lock_guard().await; - let block_digest = block_selector.as_digest(&state).await?; - state - .chain - .archival_state() - .get_block_header(block_digest) - .await + let archival_state = state.chain.archival_state(); + let block_digest = block_selector.as_digest(archival_state).await?; + archival_state.get_block_header(block_digest).await } - // 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( mut self, _context: tarpc::context::Context, key_type: KeyType, ) -> ReceivingAddress { - let mut global_state_mut = self.state.lock_guard_mut().await; - - let address = global_state_mut - .wallet_state - .next_unused_spending_key(key_type) - .to_address(); - - // persist wallet state to disk - global_state_mut.persist_wallet().await.expect("flushed"); + let span = tracing::debug_span!("rpc::next_receiving_address"); + let _enter = span.enter(); - address + self.state.next_spending_key(key_type).await.to_address() } // documented in trait. do not add doc-comment. async fn mempool_tx_count(self, _context: tarpc::context::Context) -> usize { + let span = tracing::debug_span!("rpc::mempool_tx_count"); + let _enter = span.enter(); + 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 { + let span = tracing::debug_span!("rpc::mempool_size"); + let _enter = span.enter(); + self.state.lock_guard().await.mempool.get_size() } @@ -510,6 +575,9 @@ impl RPC for NeptuneRPCServer { self, _context: tarpc::context::Context, ) -> Vec<(Digest, BlockHeight, Timestamp, NeptuneCoins)> { + let span = tracing::debug_span!("rpc::history"); + let _enter = span.enter(); + let history = self.state.lock_guard().await.get_balance_history().await; // sort @@ -528,6 +596,9 @@ impl RPC for NeptuneRPCServer { self, _context: tarpc::context::Context, ) -> DashBoardOverviewDataFromClient { + let span = tracing::debug_span!("rpc::dashboard_overview_data"); + let _enter = span.enter(); + let now = Timestamp::now(); let state = self.state.lock_guard().await; let tip_digest = state.chain.light_state().hash(); @@ -566,6 +637,9 @@ impl RPC for NeptuneRPCServer { // // documented in trait. do not add doc-comment. async fn clear_all_standings(mut self, _: context::Context) { + let span = tracing::debug_span!("rpc::clear_all_standings"); + let _enter = span.enter(); + let mut global_state_mut = self.state.lock_guard_mut().await; global_state_mut .net @@ -589,6 +663,9 @@ impl RPC for NeptuneRPCServer { // // documented in trait. do not add doc-comment. async fn clear_standing_by_ip(mut self, _: context::Context, ip: IpAddr) { + let span = tracing::debug_span!("rpc::clear_standing_by_ip"); + let _enter = span.enter(); + let mut global_state_mut = self.state.lock_guard_mut().await; global_state_mut .net @@ -609,117 +686,97 @@ impl RPC for NeptuneRPCServer { .expect("flushed DBs"); } + // TODO: add an endpoint to get recommended fee density. + // // documented in trait. do not add doc-comment. - async fn send( - self, - ctx: context::Context, - amount: NeptuneCoins, - address: ReceivingAddress, - owned_utxo_notify_method: UtxoNotifyMethod, - fee: NeptuneCoins, - ) -> Option { - self.send_to_many(ctx, vec![(address, amount)], owned_utxo_notify_method, fee) + async fn send(mut self, _ctx: context::Context, tx_params: TxParams) -> Result { + let span = tracing::debug_span!("rpc::send"); + let _enter = span.enter(); + + let transaction = self + .state + .send(tx_params) + .await + .map_err(|e| e.to_string())?; + + // Send transaction message to main + let tx_hash = Hash::hash(&transaction); + self.rpc_server_to_main_tx + .send(RPCServerToMain::Send(Box::new(transaction))) .await + .map(|_| tx_hash) + .map_err(|e| e.to_string())?; + + Ok(tx_hash) + } + + async fn claim_utxo( + mut self, + _ctx: context::Context, + utxo_transfer_encrypted_str: String, + ) -> Result<(), String> { + let span = tracing::debug_span!("rpc::claim_utxo"); + let _enter = span.enter(); + + self.state + .claim_utxo(utxo_transfer_encrypted_str) + .await + .map_err(|e| e.to_string()) } - // 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( + async fn generate_tx_params( mut self, _ctx: context::Context, - outputs: Vec<(ReceivingAddress, NeptuneCoins)>, - owned_utxo_notify_method: UtxoNotifyMethod, + outputs: Vec, fee: NeptuneCoins, - ) -> Option { - let span = tracing::debug_span!("Constructing transaction"); + owned_utxo_notify_method: OwnedUtxoNotifyMethod, + unowned_utxo_notify_method: UnownedUtxoNotifyMethod, + ) -> Result<(TxParams, Vec), String> { + let span = tracing::debug_span!("rpc::generate_tx_params"); let _enter = span.enter(); - let now = Timestamp::now(); - - // 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. create_transaction() may be slow. - s.persist_wallet().await.expect("flushed"); - key - }; + self.state + .generate_tx_params( + outputs, + fee, + owned_utxo_notify_method, + unowned_utxo_notify_method, + ) + .await + .map_err(|e| e.to_string()) + } - let state = self.state.lock_guard().await; - 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); - return None; - } - }; + async fn generate_tx_params_from_tx_outputs( + self, + _: context::Context, + tx_output_list: TxOutputList, + change_key: SpendingKey, + change_utxo_notify_method: OwnedUtxoNotifyMethod, + fee: NeptuneCoins, + ) -> Result { + let span = tracing::debug_span!("rpc::generate_tx_params"); + let _enter = span.enter(); - // Create the transaction - // - // Note that create_transaction() does not modify any state and only - // requires acquiring a read-lock which does not block other tasks. - // This is important because internally it calls prove() which is a very - // lengthy operation. - // - // note: A change output will be added to tx_outputs if needed. - let transaction = match state - .create_transaction( - &mut tx_outputs, + self.state + .lock_guard() + .await + .generate_tx_params_from_tx_outputs( + tx_output_list, change_key, - owned_utxo_notify_method, + change_utxo_notify_method, fee, - now, + Timestamp::now(), ) .await - { - Ok(tx) => tx, - Err(err) => { - tracing::error!("Could not create transaction: {}", err); - return None; - } - }; - drop(state); - - // 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 wallet state out to disk. - gsm.persist_wallet().await.expect("flushed wallet"); - } - - // Send transaction message to main - let response: Result<(), SendError> = self - .rpc_server_to_main_tx - .send(RPCServerToMain::Send(Box::new(transaction.clone()))) - .await; - - match response { - Ok(_) => Some(Hash::hash(&transaction)), - Err(e) => { - tracing::error!("Could not send Tx to main task: error: {}", e.to_string()); - None - } - } + .map_err(|e| e.to_string()) } // documented in trait. do not add doc-comment. async fn shutdown(self, _: context::Context) -> bool { + let span = tracing::debug_span!("rpc::shutdown"); + let _enter = span.enter(); + // 1. Send shutdown message to main let response = self .rpc_server_to_main_tx @@ -732,6 +789,9 @@ impl RPC for NeptuneRPCServer { // documented in trait. do not add doc-comment. async fn pause_miner(self, _context: tarpc::context::Context) { + let span = tracing::debug_span!("rpc::pause_miner"); + let _enter = span.enter(); + if self.state.cli().mine { let _ = self .rpc_server_to_main_tx @@ -744,6 +804,9 @@ impl RPC for NeptuneRPCServer { // documented in trait. do not add doc-comment. async fn restart_miner(self, _context: tarpc::context::Context) { + let span = tracing::debug_span!("rpc::pause_miner"); + let _enter = span.enter(); + if self.state.cli().mine { let _ = self .rpc_server_to_main_tx @@ -756,6 +819,9 @@ impl RPC for NeptuneRPCServer { // documented in trait. do not add doc-comment. async fn prune_abandoned_monitored_utxos(mut self, _context: tarpc::context::Context) -> usize { + let span = tracing::debug_span!("rpc::prune_abandoned_monitored_utxos"); + let _enter = span.enter(); + let mut global_state_mut = self.state.lock_guard_mut().await; const DEFAULT_MUTXO_PRUNE_DEPTH: usize = 200; @@ -785,6 +851,9 @@ impl RPC for NeptuneRPCServer { self, _context: ::tarpc::context::Context, ) -> Vec { + let span = tracing::debug_span!("rpc::list_own_coins"); + let _enter = span.enter(); + self.state .lock_guard() .await @@ -795,6 +864,9 @@ impl RPC for NeptuneRPCServer { // documented in trait. do not add doc-comment. async fn cpu_temp(self, _context: tarpc::context::Context) -> Option { + let span = tracing::debug_span!("rpc::cpu_temp"); + let _enter = span.enter(); + Self::cpu_temp_inner() } } @@ -806,22 +878,23 @@ mod rpc_server_tests { use std::net::SocketAddr; use anyhow::Result; + use clap::ValueEnum; + use itertools::Itertools; use num_traits::One; use num_traits::Zero; use rand::Rng; - use strum::IntoEnumIterator; use tracing_test::traced_test; use ReceivingAddress; use crate::config_models::network::Network; use crate::database::storage::storage_vec::traits::*; + use crate::models::blockchain::transaction::TxOutput; use crate::models::peer::PeerSanctionReason; use crate::models::state::wallet::address::generation_address::GenerationReceivingAddress; - use crate::models::state::wallet::expected_utxo::ExpectedUtxo; - use crate::models::state::wallet::expected_utxo::UtxoNotifier; + use crate::models::state::wallet::address::symmetric_key::SymmetricKey; use crate::models::state::wallet::WalletSecret; use crate::rpc_server::NeptuneRPCServer; - use crate::tests::shared::make_mock_block_with_valid_pow; + use crate::tests::shared::mine_block_to_wallet; use crate::tests::shared::mock_genesis_global_state; use crate::Block; use crate::RPC_CHANNEL_CAPACITY; @@ -832,7 +905,7 @@ mod rpc_server_tests { network: Network, wallet_secret: WalletSecret, peer_count: u8, - ) -> (NeptuneRPCServer, GlobalStateLock) { + ) -> NeptuneRPCServer { let global_state_lock = mock_genesis_global_state(network, peer_count, wallet_secret).await; let (dummy_tx, mut dummy_rx) = tokio::sync::mpsc::channel::(RPC_CHANNEL_CAPACITY); @@ -843,22 +916,19 @@ mod rpc_server_tests { } }); - ( - NeptuneRPCServer { - socket_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080), - state: global_state_lock.clone(), - rpc_server_to_main_tx: dummy_tx, - }, - global_state_lock, - ) + NeptuneRPCServer { + socket_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080), + state: global_state_lock, + rpc_server_to_main_tx: dummy_tx, + } } #[tokio::test] async fn network_response_is_consistent() -> Result<()> { // Verify that a wallet not receiving a premine is empty at startup - for network in Network::iter() { - let (rpc_server, _) = test_rpc_server(network, WalletSecret::new_random(), 2).await; - assert_eq!(network, rpc_server.network(context::current()).await); + for network in Network::value_variants() { + let rpc_server = test_rpc_server(*network, WalletSecret::new_random(), 2).await; + assert_eq!(*network, rpc_server.network(context::current()).await); } Ok(()) @@ -870,10 +940,15 @@ mod rpc_server_tests { // We don't care about the actual response data in this test, just that the // requests do not crash the server. - let network = Network::RegTest; - let (rpc_server, _) = test_rpc_server(network, WalletSecret::new_random(), 2).await; + let network = Network::Regtest; + let mut rpc_server = test_rpc_server(network, WalletSecret::new_random(), 2).await; + + mine_block_to_wallet(&mut rpc_server.state).await?; + let ctx = context::current(); + let _ = rpc_server.clone().network(ctx).await; + let _ = rpc_server.clone().data_directory(ctx).await; let _ = rpc_server.clone().own_listen_address_for_peers(ctx).await; let _ = rpc_server.clone().own_instance_id(ctx).await; let _ = rpc_server.clone().block_height(ctx).await; @@ -896,10 +971,6 @@ 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, 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; @@ -912,24 +983,45 @@ mod rpc_server_tests { .clone() .clear_standing_by_ip(ctx, "127.0.0.1".parse().unwrap()) .await; - let _ = rpc_server + + let (tx_params, _) = rpc_server .clone() - .send( + .generate_tx_params( ctx, - NeptuneCoins::one(), - own_receiving_address.clone(), - UtxoNotifyMethod::OffChain, - NeptuneCoins::one(), + vec![( + GenerationReceivingAddress::derive_from_seed(rand::random()).into(), + NeptuneCoins::new(1), + )], + NeptuneCoins::one_nau(), + OwnedUtxoNotifyMethod::OffChainSerialized, + UnownedUtxoNotifyMethod::default(), ) - .await; + .await + .unwrap(); + let _ = rpc_server .clone() - .send_to_many( + .generate_tx_params_from_tx_outputs( ctx, - vec![(own_receiving_address, NeptuneCoins::one())], - UtxoNotifyMethod::OffChain, - NeptuneCoins::one(), + TxOutput::new_random(5u32.into()).into(), + SymmetricKey::from_seed(rand::random()).into(), + OwnedUtxoNotifyMethod::OffChain, + NeptuneCoins::one_nau(), ) + .await + .unwrap(); + + let utxo_transfer_encrypted = tx_params + .tx_output_list() + .utxo_transfer_iter() + .next() + .unwrap(); + + let _ = rpc_server.clone().send(ctx, tx_params).await; + + let _ = rpc_server + .clone() + .claim_utxo(ctx, utxo_transfer_encrypted.to_bech32m(network)?) .await; let _ = rpc_server.clone().pause_miner(ctx).await; let _ = rpc_server.clone().restart_miner(ctx).await; @@ -946,7 +1038,7 @@ mod rpc_server_tests { #[tokio::test] async fn balance_is_zero_at_init() -> Result<()> { // Verify that a wallet not receiving a premine is empty at startup - let (rpc_server, _) = test_rpc_server(Network::Alpha, WalletSecret::new_random(), 2).await; + let rpc_server = test_rpc_server(Network::Alpha, WalletSecret::new_random(), 2).await; let balance = rpc_server.synced_balance(context::current()).await; assert!(balance.is_zero()); @@ -957,10 +1049,9 @@ mod rpc_server_tests { #[traced_test] #[tokio::test] async fn clear_ip_standing_test() -> Result<()> { - let (rpc_server, mut state_lock) = - test_rpc_server(Network::Alpha, WalletSecret::new_random(), 2).await; + let mut rpc_server = test_rpc_server(Network::Alpha, WalletSecret::new_random(), 2).await; let rpc_request_context = context::current(); - let global_state = state_lock.lock_guard().await; + let global_state = rpc_server.state.lock_guard().await; let peer_address_0 = global_state.net.peer_map.values().collect::>()[0].connected_address; let peer_address_1 = @@ -979,7 +1070,7 @@ mod rpc_server_tests { // sanction both let (standing_0, standing_1) = { - let mut global_state_mut = state_lock.lock_guard_mut().await; + let mut global_state_mut = rpc_server.state.lock_guard_mut().await; global_state_mut .net @@ -1012,7 +1103,7 @@ mod rpc_server_tests { ); { - let mut global_state_mut = state_lock.lock_guard_mut().await; + let mut global_state_mut = rpc_server.state.lock_guard_mut().await; global_state_mut .net @@ -1037,7 +1128,7 @@ mod rpc_server_tests { // Verify expected initial conditions { - let global_state = state_lock.lock_guard().await; + let global_state = rpc_server.state.lock_guard().await; let peer_standing_0 = global_state .net .get_peer_standing_from_database(peer_address_0.ip()) @@ -1061,7 +1152,7 @@ mod rpc_server_tests { // Verify expected resulting conditions in database { - let global_state = state_lock.lock_guard().await; + let global_state = rpc_server.state.lock_guard().await; let peer_standing_0 = global_state .net .get_peer_standing_from_database(peer_address_0.ip()) @@ -1100,9 +1191,8 @@ mod rpc_server_tests { #[tokio::test] async fn clear_all_standings_test() -> Result<()> { // Create initial conditions - let (rpc_server, mut state_lock) = - test_rpc_server(Network::Alpha, WalletSecret::new_random(), 2).await; - let mut state = state_lock.lock_guard_mut().await; + let mut rpc_server = test_rpc_server(Network::Alpha, WalletSecret::new_random(), 2).await; + let mut state = rpc_server.state.lock_guard_mut().await; let peer_address_0 = state.net.peer_map.values().collect::>()[0].connected_address; let peer_address_1 = state.net.peer_map.values().collect::>()[1].connected_address; @@ -1132,7 +1222,8 @@ mod rpc_server_tests { // Verify expected initial conditions { - let peer_standing_0 = state_lock + let peer_standing_0 = rpc_server + .state .lock_guard_mut() .await .net @@ -1143,7 +1234,8 @@ mod rpc_server_tests { } { - let peer_standing_1 = state_lock + let peer_standing_1 = rpc_server + .state .lock_guard_mut() .await .net @@ -1167,7 +1259,7 @@ mod rpc_server_tests { .clear_all_standings(rpc_request_context) .await; - let state = state_lock.lock_guard().await; + let state = rpc_server.state.lock_guard().await; // Verify expected resulting conditions in database { @@ -1212,9 +1304,8 @@ mod rpc_server_tests { #[traced_test] #[tokio::test] async fn utxo_digest_test() { - let (rpc_server, state_lock) = - test_rpc_server(Network::Alpha, WalletSecret::new_random(), 2).await; - let global_state = state_lock.lock_guard().await; + let rpc_server = test_rpc_server(Network::Alpha, WalletSecret::new_random(), 2).await; + let global_state = rpc_server.state.lock_guard().await; let aocl_leaves = global_state .chain .archival_state() @@ -1233,6 +1324,7 @@ mod rpc_server_tests { .is_some()); assert!(rpc_server + .clone() .utxo_digest(context::current(), aocl_leaves) .await .is_none()); @@ -1241,10 +1333,9 @@ mod rpc_server_tests { #[traced_test] #[tokio::test] async fn block_info_test() { - let network = Network::RegTest; - let (rpc_server, state_lock) = - test_rpc_server(network, WalletSecret::new_random(), 2).await; - let global_state = state_lock.lock_guard().await; + let network = Network::Regtest; + let rpc_server = test_rpc_server(network, WalletSecret::new_random(), 2).await; + let global_state = rpc_server.state.lock_guard().await; let ctx = context::current(); let genesis_hash = global_state.chain.archival_state().genesis_block().hash(); @@ -1320,10 +1411,9 @@ mod rpc_server_tests { #[traced_test] #[tokio::test] async fn block_digest_test() { - let network = Network::RegTest; - let (rpc_server, state_lock) = - test_rpc_server(network, WalletSecret::new_random(), 2).await; - let global_state = state_lock.lock_guard().await; + let network = Network::Regtest; + let rpc_server = test_rpc_server(network, WalletSecret::new_random(), 2).await; + let global_state = rpc_server.state.lock_guard().await; let ctx = context::current(); let genesis_hash = Block::genesis_block(network).hash(); @@ -1389,7 +1479,7 @@ mod rpc_server_tests { // On your local machine, this should return a temperature but in CI, // the RPC call returns `None`, so we only verify that the call doesn't // crash the host machine, we don't verify that any value is returned. - let (rpc_server, _) = test_rpc_server(Network::Alpha, WalletSecret::new_random(), 2).await; + let rpc_server = test_rpc_server(Network::Alpha, WalletSecret::new_random(), 2).await; let _current_server_temperature = rpc_server.cpu_temp(context::current()).await; } @@ -1397,42 +1487,13 @@ mod rpc_server_tests { #[tokio::test] async fn send_to_many_test() -> Result<()> { // --- Init. Basics --- - let network = Network::RegTest; - let (rpc_server, mut state_lock) = - test_rpc_server(network, WalletSecret::new_random(), 2).await; + let network = Network::Regtest; + let mut rpc_server = test_rpc_server(network, WalletSecret::new_random(), 2).await; let ctx = context::current(); let mut rng = rand::thread_rng(); - // --- Init. get wallet spending key --- - let genesis_block = Block::genesis_block(network); - let wallet_spending_key = state_lock - .lock_guard_mut() - .await - .wallet_state - .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().try_into()?, - rng.gen(), - ); - - // --- Init. append the block to blockchain --- - state_lock - .lock_guard_mut() - .await - .set_new_self_mined_tip( - block_1, - ExpectedUtxo::new( - cb_utxo, - cb_output_randomness, - wallet_spending_key.privacy_preimage(), - UtxoNotifier::OwnMiner, - ), - ) - .await?; + mine_block_to_wallet(&mut rpc_server.state).await?; // --- Setup. generate an output that our wallet cannot claim. --- let output1 = ( @@ -1442,20 +1503,31 @@ mod rpc_server_tests { // --- Setup. generate an output that our wallet can claim. --- let output2 = { - let spending_key = state_lock - .lock_guard_mut() - .await - .wallet_state - .next_unused_spending_key(KeyType::Generation); - (spending_key.to_address(), NeptuneCoins::new(25)) + let address = rpc_server + .clone() + .next_receiving_address(ctx, KeyType::Generation) + .await; + (address, NeptuneCoins::new(25)) }; // --- Setup. assemble outputs and fee --- - let outputs = vec![output1, output2]; + // let outputs = vec![output1, output2]; let fee = NeptuneCoins::new(1); + let (tx_params, _) = rpc_server + .clone() + .generate_tx_params( + ctx, + vec![output1, output2], + fee, + OwnedUtxoNotifyMethod::OffChain, + UnownedUtxoNotifyMethod::default(), + ) + .await + .map_err(|e| anyhow::anyhow!(e))?; // --- Store: store num expected utxo before spend --- - let num_expected_utxo = state_lock + let num_expected_utxo = rpc_server + .state .lock_guard() .await .wallet_state @@ -1464,19 +1536,20 @@ mod rpc_server_tests { .len() .await; - // --- Operation: perform send_to_many - let result = rpc_server - .clone() - .send_to_many(ctx, outputs, UtxoNotifyMethod::OffChain, fee) - .await; + // --- Store: store num tx_in_mempool before spend --- + let num_tx_in_mempool = rpc_server.state.lock_guard().await.mempool.len(); + + // --- Operation: perform send + let result = rpc_server.clone().send(ctx, tx_params).await; // --- Test: verify op returns a value. - assert!(result.is_some()); + assert!(result.is_ok()); // --- Test: verify expected_utxos.len() has increased by 2. // (one off-chain utxo + one change utxo) assert_eq!( - state_lock + rpc_server + .state .lock_guard() .await .wallet_state @@ -1487,6 +1560,293 @@ mod rpc_server_tests { num_expected_utxo + 2 ); + // --- Test: verify num_tx_in_mempool has increased by 1. + assert_eq!( + rpc_server.state.lock_guard().await.mempool.len(), + num_tx_in_mempool + 1, + ); + Ok(()) } + + #[traced_test] + #[allow(clippy::needless_return)] + #[tokio::test] + async fn claim_utxo_owned_before_confirmed() -> Result<()> { + worker::claim_utxo_owned(false).await + } + + #[traced_test] + #[allow(clippy::needless_return)] + #[tokio::test] + async fn claim_utxo_owned_after_confirmed() -> Result<()> { + worker::claim_utxo_owned(true).await + } + + #[traced_test] + #[allow(clippy::needless_return)] + #[tokio::test] + async fn claim_utxo_unowned_before_confirmed() -> Result<()> { + worker::claim_utxo_unowned(false).await + } + + #[traced_test] + #[allow(clippy::needless_return)] + #[tokio::test] + async fn claim_utxo_unowned_after_confirmed() -> Result<()> { + worker::claim_utxo_unowned(true).await + } + + mod worker { + use super::*; + + pub async fn claim_utxo_unowned(claim_after_confirmed: bool) -> Result<()> { + let network = Network::Regtest; + + // bob's node + let (pay_to_bob_outputs, bob_rpc_server) = { + let rpc_server = test_rpc_server(network, WalletSecret::new_random(), 2).await; + + let receiving_address_generation = rpc_server + .clone() + .next_receiving_address(context::current(), KeyType::Generation) + .await; + let receiving_address_symmetric = rpc_server + .clone() + .next_receiving_address(context::current(), KeyType::Symmetric) + .await; + + let pay_to_bob_outputs = vec![ + (receiving_address_generation, NeptuneCoins::new(1)), + (receiving_address_symmetric, NeptuneCoins::new(2)), + ]; + + (pay_to_bob_outputs, rpc_server) + }; + + // alice's node + let (blocks, alice_utxo_transfer_encrypted_to_bob_list, bob_amount) = { + let mut rpc_server = test_rpc_server(network, WalletSecret::new_random(), 2).await; + + let mut blocks = vec![]; + + // mine a block to obtain some coinbase coins for spending. + blocks.push(mine_block_to_wallet(&mut rpc_server.state).await?); + + let fee = NeptuneCoins::zero(); + let bob_amount: NeptuneCoins = pay_to_bob_outputs.iter().map(|(_, amt)| *amt).sum(); + + let (tx_params, _) = rpc_server + .clone() + .generate_tx_params( + context::current(), + pay_to_bob_outputs, + fee, + OwnedUtxoNotifyMethod::default(), + UnownedUtxoNotifyMethod::OffChainSerialized, + ) + .await + .map_err(|e| anyhow::anyhow!(e))?; + + let utxo_transfer_list = tx_params + .tx_output_list() + .utxo_transfer_iter() + .collect_vec(); + + let _ = rpc_server.clone().send(context::current(), tx_params).await; + + // mine two more blocks + blocks.push(mine_block_to_wallet(&mut rpc_server.state).await?); + blocks.push(mine_block_to_wallet(&mut rpc_server.state).await?); + + (blocks, utxo_transfer_list, bob_amount) + }; + + // bob's node claims each utxo + { + let mut state = bob_rpc_server.state.clone(); + + state.store_block(blocks[0].clone()).await?; + + if claim_after_confirmed { + state.store_block(blocks[1].clone()).await?; + state.store_block(blocks[2].clone()).await?; + } + + for utxo_transfer_encrypted in alice_utxo_transfer_encrypted_to_bob_list.iter() { + bob_rpc_server + .clone() + .claim_utxo( + context::current(), + utxo_transfer_encrypted.to_bech32m(network)?, + ) + .await + .map_err(|e| anyhow::anyhow!(e))?; + } + + assert_eq!( + vec![ + NeptuneCoins::new(1), // claimed via generation addr + NeptuneCoins::new(2), // claimed via symmetric addr + ], + state + .lock_guard() + .await + .wallet_state + .wallet_db + .expected_utxos() + .get_all() + .await + .iter() + .map(|eu| eu.utxo.get_native_currency_amount()) + .collect_vec() + ); + + if !claim_after_confirmed { + assert_eq!( + NeptuneCoins::zero(), + bob_rpc_server + .clone() + .synced_balance(context::current()) + .await, + ); + state.store_block(blocks[1].clone()).await?; + state.store_block(blocks[2].clone()).await?; + } + + assert_eq!( + bob_amount, + bob_rpc_server.synced_balance(context::current()).await, + ); + } + + Ok(()) + } + + pub async fn claim_utxo_owned(claim_after_confirmed: bool) -> Result<()> { + let network = Network::Regtest; + let mut alice_rpc_server = + test_rpc_server(network, WalletSecret::new_random(), 2).await; + let mut bob_rpc_server = test_rpc_server(network, WalletSecret::new_random(), 2).await; + + let block1 = mine_block_to_wallet(&mut bob_rpc_server.state).await?; + alice_rpc_server.state.store_block(block1).await?; + + let ctx = context::current(); + + let receiving_address_generation = bob_rpc_server + .clone() + .next_receiving_address(context::current(), KeyType::Generation) + .await; + let receiving_address_symmetric = bob_rpc_server + .clone() + .next_receiving_address(context::current(), KeyType::Symmetric) + .await; + + let pay_to_self_outputs = vec![ + (receiving_address_generation, NeptuneCoins::new(1)), + (receiving_address_symmetric, NeptuneCoins::new(2)), + ]; + + let (tx_params, _) = bob_rpc_server + .clone() + .generate_tx_params( + ctx, + pay_to_self_outputs, + NeptuneCoins::new(1), + OwnedUtxoNotifyMethod::OffChainSerialized, + UnownedUtxoNotifyMethod::default(), + ) + .await + .map_err(|e| anyhow::anyhow!(e))?; + + let tx_output_list = tx_params.tx_output_list().clone(); + + let _ = bob_rpc_server.clone().send(ctx, tx_params.clone()).await; + + // simulate that bob sends tx to alice's mempool via p2p network + let _ = alice_rpc_server.clone().send(ctx, tx_params).await; + + // alice mines 2 more blocks. block2 confirms the sent tx. + let block2 = mine_block_to_wallet(&mut alice_rpc_server.state).await?; + let block3 = mine_block_to_wallet(&mut alice_rpc_server.state).await?; + + if claim_after_confirmed { + // bob applies the blocks before claiming utxos. + bob_rpc_server.state.store_block(block2.clone()).await?; + bob_rpc_server.state.store_block(block3.clone()).await?; + } + + for utxo_transfer_encrypted in tx_output_list.utxo_transfer_iter() { + bob_rpc_server + .clone() + .claim_utxo( + context::current(), + utxo_transfer_encrypted.to_bech32m(network)?, + ) + .await + .map_err(|e| anyhow::anyhow!(e))?; + } + + assert_eq!( + vec![ + NeptuneCoins::new(100), // from block1 coinbase + NeptuneCoins::new(1), // claimed via generation addr + NeptuneCoins::new(2), // claimed via symmetric addr + NeptuneCoins::new(96) // change (symmetric addr) + ], + bob_rpc_server + .state + .lock_guard() + .await + .wallet_state + .wallet_db + .expected_utxos() + .get_all() + .await + .iter() + .map(|eu| eu.utxo.get_native_currency_amount()) + .collect_vec() + ); + + if !claim_after_confirmed { + // bob hasn't applied blocks 2,3. balance should be 100 + assert_eq!( + NeptuneCoins::new(100), + bob_rpc_server + .clone() + .synced_balance(context::current()) + .await, + ); + // bob applies the blocks after claiming utxos. + bob_rpc_server.state.store_block(block2).await?; + bob_rpc_server.state.store_block(block3).await?; + } + + // final balance should be 99. + // +100 coinbase + // -100 coinbase spent + // +1 self-send via Generation + // +2 self-send via Symmetric + // +96 change (less fee == 1) + assert_eq!( + NeptuneCoins::new(99), + bob_rpc_server.synced_balance(context::current()).await, + ); + + // todo: test that claim_utxo() correctly handles case when the + // claimed utxo has already been spent. + // + // in normal wallet usage this would not happen. However it + // is possible if bob were to claim a utxo with wallet A, + // spend the utxo and then restore wallet B from A's seed. + // When bob performs claim_utxo() in wallet B the balance + // should reflect that the utxo was already spent. + // + // this is a bit tricky to test, as it requires using a + // different data directory for wallet B and test infrastructure + // isn't setup for that. + Ok(()) + } + } } diff --git a/src/tests/shared.rs b/src/tests/shared.rs index 33b6533b..3d5354e0 100644 --- a/src/tests/shared.rs +++ b/src/tests/shared.rs @@ -34,7 +34,6 @@ use tokio_serde::Serializer; use tokio_util::codec::Encoder; use tokio_util::codec::LengthDelimitedCodec; use twenty_first::math::b_field_element::BFieldElement; -use twenty_first::math::bfield_codec::BFieldCodec; use twenty_first::math::digest::Digest; use twenty_first::util_types::algebraic_hasher::AlgebraicHasher; use twenty_first::util_types::mmr::mmr_trait::Mmr; @@ -77,7 +76,6 @@ use crate::models::peer::PeerInfo; use crate::models::peer::PeerMessage; use crate::models::peer::PeerStanding; use crate::models::state::archival_state::ArchivalState; -use crate::models::state::blockchain_state::BlockchainArchivalState; use crate::models::state::blockchain_state::BlockchainState; use crate::models::state::light_state::LightState; use crate::models::state::mempool::Mempool; @@ -195,7 +193,7 @@ pub async fn mock_genesis_global_state( peer_map.insert(peer_address, get_dummy_peer(peer_address)); } let networking_state = NetworkingState::new(peer_map, peer_db, syncing); - let genesis_block = archival_state.get_tip().await; + let genesis_block = archival_state.tip(); // Sanity check assert_eq!(archival_state.genesis_block().hash(), genesis_block.hash()); @@ -205,11 +203,8 @@ pub async fn mock_genesis_global_state( "Genesis light state MSA hash: {}", light_state.body().mutator_set_accumulator.hash() ); - let blockchain_state = BlockchainState::Archival(BlockchainArchivalState { - light_state, - archival_state, - }); let mempool = Mempool::new(ByteSize::gb(1), genesis_block.hash()); + let blockchain_state = BlockchainState::Archival(archival_state); let cli_args = cli_args::Args { network, ..Default::default() @@ -267,6 +262,8 @@ pub async fn add_block_to_light_state( let previous_pow_family = light_state.kernel.header.proof_of_work_family; if previous_pow_family < new_block.kernel.header.proof_of_work_family { light_state.set_block(new_block); + } else if new_block == *light_state { + // no-op. light-state already has the block. } else { panic!("Attempted to add to light state an older block than the current light state block"); } @@ -722,17 +719,11 @@ pub async fn make_mock_transaction_with_generation_key( let type_scripts = vec![TypeScript::native_currency()]; - let spending_key_unlock_keys = tx_inputs - .spending_keys_iter() - .into_iter() - .map(|k| k.unlock_key().encode()) - .collect_vec(); - let primitive_witness = transaction::primitive_witness::PrimitiveWitness { input_utxos: SaltedUtxos::new(tx_inputs.utxos()), type_scripts, input_lock_scripts: tx_inputs.lock_scripts(), - lock_script_witnesses: spending_key_unlock_keys, + lock_script_witnesses: tx_inputs.lock_script_witnesses(), input_membership_proofs: tx_inputs.ms_membership_proofs(), output_utxos: SaltedUtxos::new(tx_outputs.utxos()), mutator_set_accumulator: tip_msa, @@ -989,3 +980,27 @@ pub async fn mock_genesis_archival_state( (archival_state, peer_db, data_dir) } + +// this will create and store the next block including any transactions +// presently in the mempool. The coinbase will go to our own wallet. +// +// the stored block does NOT have valid proof-of-work. +pub async fn mine_block_to_wallet(global_state_lock: &mut GlobalStateLock) -> Result { + let state = global_state_lock.lock_guard().await; + let tip_block = state.chain.light_state(); + + let timestamp = Timestamp::now(); + let (transaction, coinbase_expected_utxo) = + crate::mine_loop::create_block_transaction(tip_block, &state, timestamp); + + let (header, body) = + crate::mine_loop::make_block_template(tip_block, transaction, timestamp, None); + let block = Block::new(header, body, Block::mk_std_block_type(None)); + drop(state); + + global_state_lock + .store_coinbase_block(block.clone(), coinbase_expected_utxo) + .await?; + + Ok(block) +}