From 7fe3f481f3a6fae507005b7a7677816241c2a6da Mon Sep 17 00:00:00 2001 From: coderofstuff <114628839+coderofstuff@users.noreply.github.com> Date: Sat, 16 Mar 2024 09:01:24 -0600 Subject: [PATCH] Implement UTXO Return Address RPC --- Cargo.lock | 2 + cli/src/modules/rpc.rs | 15 ++ components/consensusmanager/Cargo.toml | 1 + components/consensusmanager/src/session.rs | 10 + consensus/Cargo.toml | 1 + consensus/core/src/api/mod.rs | 6 + consensus/core/src/lib.rs | 1 + consensus/core/src/return_address.rs | 15 ++ consensus/src/consensus/mod.rs | 6 + .../pipeline/virtual_processor/processor.rs | 199 +++++++++++++++++- rpc/core/src/api/ops.rs | 2 + rpc/core/src/api/rpc.rs | 12 ++ rpc/core/src/error.rs | 5 +- rpc/core/src/model/message.rs | 63 ++++++ rpc/grpc/client/src/lib.rs | 1 + rpc/grpc/core/Cargo.toml | 1 + rpc/grpc/core/proto/messages.proto | 2 + rpc/grpc/core/proto/rpc.proto | 12 +- rpc/grpc/core/src/convert/kaspad.rs | 2 + rpc/grpc/core/src/convert/message.rs | 22 +- rpc/grpc/core/src/ops.rs | 1 + .../server/src/request_handler/factory.rs | 1 + rpc/grpc/server/src/tests/rpc_core_mock.rs | 8 + rpc/service/src/service.rs | 14 ++ rpc/wrpc/client/src/client.rs | 1 + rpc/wrpc/server/src/router.rs | 2 + .../src/daemon_integration_tests.rs | 20 +- testing/integration/src/rpc_tests.rs | 17 ++ wallet/core/src/tests/rpc_core_mock.rs | 8 + 29 files changed, 440 insertions(+), 10 deletions(-) create mode 100644 consensus/core/src/return_address.rs diff --git a/Cargo.lock b/Cargo.lock index 8bc4c86ea7..7c70d073a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2562,6 +2562,7 @@ dependencies = [ "futures", "futures-util", "itertools 0.13.0", + "kaspa-addresses", "kaspa-consensus-core", "kaspa-consensus-notify", "kaspa-core", @@ -2676,6 +2677,7 @@ dependencies = [ "faster-hex", "futures", "h2 0.4.6", + "kaspa-addresses", "kaspa-consensus-core", "kaspa-core", "kaspa-notify", diff --git a/cli/src/modules/rpc.rs b/cli/src/modules/rpc.rs index cf6bc6bd20..c721ed6223 100644 --- a/cli/src/modules/rpc.rs +++ b/cli/src/modules/rpc.rs @@ -269,6 +269,21 @@ impl Rpc { let result = rpc.get_current_block_color_call(None, GetCurrentBlockColorRequest { hash }).await?; self.println(&ctx, result); } + RpcApiOps::GetUtxoReturnAddress => { + if argv.is_empty() || argv.len() != 2 { + return Err(Error::custom("Please specify a txid and a accepting_block_daa_score")); + } + + let txid = argv.remove(0); + let txid = RpcHash::from_hex(txid.as_str())?; + + let accepting_block_daa_score = argv.remove(0).parse::()?; + + let result = + rpc.get_utxo_return_address_call(None, GetUtxoReturnAddressRequest { txid, accepting_block_daa_score }).await?; + + self.println(&ctx, result); + } _ => { tprintln!(ctx, "rpc method exists but is not supported by the cli: '{op_str}'\r\n"); return Ok(()); diff --git a/components/consensusmanager/Cargo.toml b/components/consensusmanager/Cargo.toml index 16f6900871..c9f3645572 100644 --- a/components/consensusmanager/Cargo.toml +++ b/components/consensusmanager/Cargo.toml @@ -14,6 +14,7 @@ duration-string.workspace = true futures-util.workspace = true futures.workspace = true itertools.workspace = true +kaspa-addresses.workspace=true kaspa-consensus-core.workspace = true kaspa-consensus-notify.workspace = true kaspa-core.workspace = true diff --git a/components/consensusmanager/src/session.rs b/components/consensusmanager/src/session.rs index 8e0c6e9335..e5a7e1b2bb 100644 --- a/components/consensusmanager/src/session.rs +++ b/components/consensusmanager/src/session.rs @@ -2,6 +2,7 @@ //! //! We use newtypes in order to simplify changing the underlying lock in the future +use kaspa_addresses::Address; use kaspa_consensus_core::{ acceptance_data::AcceptanceData, api::{BlockCount, BlockValidationFutures, ConsensusApi, ConsensusStats, DynConsensus}, @@ -11,6 +12,7 @@ use kaspa_consensus_core::{ errors::consensus::ConsensusResult, header::Header, pruning::{PruningPointProof, PruningPointTrustedData, PruningPointsList}, + return_address::ReturnAddressError, trusted::{ExternalGhostdagData, TrustedBlock}, tx::{MutableTransaction, Transaction, TransactionOutpoint, UtxoEntry}, BlockHashSet, BlueWorkType, ChainPath, Hash, @@ -313,6 +315,14 @@ impl ConsensusSessionOwned { self.clone().spawn_blocking(|c| c.get_chain_block_samples()).await } + pub async fn async_get_utxo_return_script_public_key( + &self, + txid: Hash, + accepting_block_daa_score: u64, + ) -> Result { + self.clone().spawn_blocking(move |c| c.get_utxo_return_address(txid, accepting_block_daa_score)).await + } + /// Returns the antipast of block `hash` from the POV of `context`, i.e. `antipast(hash) ∩ past(context)`. /// Since this might be an expensive operation for deep blocks, we allow the caller to specify a limit /// `max_traversal_allowed` on the maximum amount of blocks to traverse for obtaining the answer diff --git a/consensus/Cargo.toml b/consensus/Cargo.toml index 443e591c8a..90aad80e1a 100644 --- a/consensus/Cargo.toml +++ b/consensus/Cargo.toml @@ -18,6 +18,7 @@ faster-hex.workspace = true futures-util.workspace = true indexmap.workspace = true itertools.workspace = true +kaspa-addresses.workspace = true kaspa-consensus-core.workspace = true kaspa-consensus-notify.workspace = true kaspa-consensusmanager.workspace = true diff --git a/consensus/core/src/api/mod.rs b/consensus/core/src/api/mod.rs index 7c244b9148..4bd49e5a59 100644 --- a/consensus/core/src/api/mod.rs +++ b/consensus/core/src/api/mod.rs @@ -1,4 +1,5 @@ use futures_util::future::BoxFuture; +use kaspa_addresses::Address; use kaspa_muhash::MuHash; use std::sync::Arc; @@ -18,6 +19,7 @@ use crate::{ }, header::Header, pruning::{PruningPointProof, PruningPointTrustedData, PruningPointsList, PruningProofMetadata}, + return_address::ReturnAddressError, trusted::{ExternalGhostdagData, TrustedBlock}, tx::{MutableTransaction, Transaction, TransactionOutpoint, UtxoEntry}, BlockHashSet, BlueWorkType, ChainPath, @@ -170,6 +172,10 @@ pub trait ConsensusApi: Send + Sync { unimplemented!() } + fn get_utxo_return_address(&self, txid: Hash, daa_score: u64) -> Result { + unimplemented!() + } + fn get_virtual_parents(&self) -> BlockHashSet { unimplemented!() } diff --git a/consensus/core/src/lib.rs b/consensus/core/src/lib.rs index e4591f2181..d92cc4b569 100644 --- a/consensus/core/src/lib.rs +++ b/consensus/core/src/lib.rs @@ -30,6 +30,7 @@ pub mod merkle; pub mod muhash; pub mod network; pub mod pruning; +pub mod return_address; pub mod sign; pub mod subnets; pub mod trusted; diff --git a/consensus/core/src/return_address.rs b/consensus/core/src/return_address.rs new file mode 100644 index 0000000000..760f3fd9d9 --- /dev/null +++ b/consensus/core/src/return_address.rs @@ -0,0 +1,15 @@ +use thiserror::Error; + +#[derive(Error, Debug, Clone)] +pub enum ReturnAddressError { + #[error("Transaction is already pruned")] + AlreadyPruned, + #[error("Transaction return address is coinbase")] + TxFromCoinbase, + #[error("Transaction not found at given accepting daa score")] + NoTxAtScore, + #[error("Transaction was found but not standard")] + NonStandard, + #[error("Transaction return address not found: {0}")] + NotFound(String), +} diff --git a/consensus/src/consensus/mod.rs b/consensus/src/consensus/mod.rs index 99719d4ac2..24323b2b71 100644 --- a/consensus/src/consensus/mod.rs +++ b/consensus/src/consensus/mod.rs @@ -41,6 +41,7 @@ use crate::{ window::{WindowManager, WindowType}, }, }; +use kaspa_addresses::Address; use kaspa_consensus_core::{ acceptance_data::AcceptanceData, api::{ @@ -65,6 +66,7 @@ use kaspa_consensus_core::{ muhash::MuHashExtensions, network::NetworkType, pruning::{PruningPointProof, PruningPointTrustedData, PruningPointsList, PruningProofMetadata}, + return_address::ReturnAddressError, trusted::{ExternalGhostdagData, TrustedBlock}, tx::{MutableTransaction, Transaction, TransactionOutpoint, UtxoEntry}, BlockHashSet, BlueWorkType, ChainPath, HashMapCustomHasher, @@ -687,6 +689,10 @@ impl ConsensusApi for Consensus { sample_headers } + fn get_utxo_return_address(&self, txid: Hash, target_daa_score: u64) -> Result { + self.virtual_processor.get_utxo_return_address(txid, target_daa_score, self.get_source(), &self.config) + } + fn get_virtual_parents(&self) -> BlockHashSet { self.lkg_virtual_state.load().parents.iter().copied().collect() } diff --git a/consensus/src/pipeline/virtual_processor/processor.rs b/consensus/src/pipeline/virtual_processor/processor.rs index 914e0a327d..facd5c6992 100644 --- a/consensus/src/pipeline/virtual_processor/processor.rs +++ b/consensus/src/pipeline/virtual_processor/processor.rs @@ -26,7 +26,7 @@ use crate::{ pruning_utxoset::PruningUtxosetStores, reachability::DbReachabilityStore, relations::{DbRelationsStore, RelationsStoreReader}, - selected_chain::{DbSelectedChainStore, SelectedChainStore}, + selected_chain::{DbSelectedChainStore, SelectedChainStore, SelectedChainStoreReader}, statuses::{DbStatusesStore, StatusesStore, StatusesStoreBatchExtensions, StatusesStoreReader}, tips::{DbTipsStore, TipsStoreReader}, utxo_diffs::{DbUtxoDiffsStore, UtxoDiffsStoreReader}, @@ -47,19 +47,21 @@ use crate::{ window::WindowManager, }, }; +use kaspa_addresses::Address; use kaspa_consensus_core::{ - acceptance_data::AcceptanceData, + acceptance_data::{AcceptanceData, MergesetBlockAcceptanceData}, api::args::{TransactionValidationArgs, TransactionValidationBatchArgs}, block::{BlockTemplate, MutableBlock, TemplateBuildMode, TemplateTransactionSelector}, blockstatus::BlockStatus::{StatusDisqualifiedFromChain, StatusUTXOValid}, coinbase::MinerData, - config::{genesis::GenesisBlock, params::ForkActivation}, + config::{genesis::GenesisBlock, params::ForkActivation, Config}, header::Header, merkle::calc_hash_merkle_root, pruning::PruningPointsList, + return_address::ReturnAddressError, tx::{MutableTransaction, Transaction}, utxo::{ - utxo_diff::UtxoDiff, + utxo_diff::{ImmutableUtxoDiff, UtxoDiff}, utxo_view::{UtxoView, UtxoViewComposition}, }, BlockHashSet, ChainPath, @@ -71,12 +73,13 @@ use kaspa_consensus_notify::{ }, root::ConsensusNotificationRoot, }; -use kaspa_consensusmanager::SessionLock; +use kaspa_consensusmanager::{SessionLock, SessionReadGuard}; use kaspa_core::{debug, info, time::unix_now, trace, warn}; use kaspa_database::prelude::{StoreError, StoreResultEmptyTuple, StoreResultExtensions}; use kaspa_hashes::Hash; use kaspa_muhash::MuHash; use kaspa_notify::{events::EventType, notifier::Notify}; +use kaspa_txscript::extract_script_pub_key_address; use once_cell::unsync::Lazy; use super::errors::{PruningImportError, PruningImportResult}; @@ -92,7 +95,7 @@ use rayon::{ }; use rocksdb::WriteBatch; use std::{ - cmp::min, + cmp::{self, min}, collections::{BinaryHeap, HashMap, VecDeque}, ops::Deref, sync::{atomic::Ordering, Arc}, @@ -1188,6 +1191,190 @@ impl VirtualStateProcessor { Ok(()) } + pub fn get_utxo_return_address( + &self, + txid: Hash, + target_daa_score: u64, + source_hash: Hash, + config: &Config, + ) -> Result { + // We need consistency between the utxo_diffs_store, block_transactions_store, selected chain and header store reads + let guard = self.pruning_lock.blocking_read(); + + let source_daa_score = self + .headers_store + .get_compact_header_data(source_hash) + .map(|compact_header| compact_header.daa_score) + .map_err(|_| ReturnAddressError::NotFound(format!("Did not find compact header for source hash {}", source_hash)))?; + + if target_daa_score < source_daa_score { + // Early exit if target daa score is lower than that of pruning point's daa score: + return Err(ReturnAddressError::AlreadyPruned); + } + + let (matching_chain_block_hash, acceptance_data) = + self.find_accepting_chain_block_hash_at_daa_score(target_daa_score, source_hash, guard)?; + + let (index, containing_acceptance) = self + .find_tx_acceptance_data_and_index_from_block_acceptance(txid, acceptance_data.clone()) + .ok_or(ReturnAddressError::NotFound(format!("Did not find containing_acceptance for tx {}", txid)))?; + + // Found Merged block containing the TXID + let tx = self + .block_transactions_store + .get(containing_acceptance.block_hash) + .map_err(|_| ReturnAddressError::NotFound(format!("Did not block {} at block tx store", containing_acceptance.block_hash))) + .and_then(|block_txs| { + block_txs.get(index).cloned().ok_or_else(|| { + ReturnAddressError::NotFound(format!( + "Did not find index {} in transactions of block {}", + index, containing_acceptance.block_hash + )) + }) + })?; + + if tx.id() != txid { + // Should never happen, but do a sanity check. This would mean something went wrong with storing block transactions + // Sanity check is necessary to guarantee that this function will never give back a wrong address (err on the side of NotFound) + warn!("Expected {} to match {} when checking block_transaction_store using array index of transaction", tx.id(), txid); + return Err(ReturnAddressError::NotFound(format!( + "Expected {} to match {} when checking block_transaction_store using array index of transaction", + tx.id(), + txid + ))); + } + + if tx.inputs.is_empty() { + // A transaction may have no inputs (like a coinbase transaction) + return Err(ReturnAddressError::TxFromCoinbase); + } + + let first_input_prev_outpoint = &tx.inputs[0].previous_outpoint; + // Expected to never fail, since we found the acceptance data and therefore there must be matching diff + let utxo_diff = self.utxo_diffs_store.get(matching_chain_block_hash).map_err(|_| { + ReturnAddressError::NotFound(format!("Did not find a utxo diff for chain block {}", matching_chain_block_hash)) + })?; + let removed_diffs = utxo_diff.removed(); + + let spk = if let Some(utxo_entry) = removed_diffs.get(first_input_prev_outpoint) { + utxo_entry.script_public_key.clone() + } else { + // This handles this rare scenario: + // - UTXO0 is spent by TX1 and creates UTXO1 + // - UTXO1 is spent by TX2 and creates UTXO2 + // - A chain block happens to accept both of these + // In this case, removed_diff wouldn't contain the outpoint of the created-and-immediately-spent UTXO + // so we use the transaction (which also has acceptance data in this block) and look at its outputs + let other_txid = first_input_prev_outpoint.transaction_id; + let (other_index, other_containing_acceptance) = self + .find_tx_acceptance_data_and_index_from_block_acceptance(other_txid, acceptance_data) + .ok_or(ReturnAddressError::NotFound( + "The other transaction's acceptance data must also be in the same block in this case".to_string(), + ))?; + let other_tx = self + .block_transactions_store + .get(other_containing_acceptance.block_hash) + .map_err(|_| { + ReturnAddressError::NotFound(format!("Did not block {} at block tx store", other_containing_acceptance.block_hash)) + }) + .and_then(|block_txs| { + block_txs.get(other_index).cloned().ok_or_else(|| { + ReturnAddressError::NotFound(format!( + "Did not find index {} in transactions of block {}", + other_index, other_containing_acceptance.block_hash + )) + }) + })?; + + other_tx.outputs[first_input_prev_outpoint.index as usize].script_public_key.clone() + }; + + if let Ok(address) = extract_script_pub_key_address(&spk, config.prefix()) { + Ok(address) + } else { + Err(ReturnAddressError::NonStandard) + } + } + + fn find_accepting_chain_block_hash_at_daa_score( + &self, + target_daa_score: u64, + source_hash: Hash, + _guard: SessionReadGuard, + ) -> Result<(Hash, Arc>), ReturnAddressError> { + let sc_read = self.selected_chain_store.read(); + + let source_index = sc_read + .get_by_hash(source_hash) + .map_err(|_| ReturnAddressError::NotFound(format!("Did not find index for hash {}", source_hash)))?; + let (tip_index, tip_hash) = + sc_read.get_tip().map_err(|_| ReturnAddressError::NotFound("Did not find tip data".to_string()))?; + let tip_daa_score = self + .headers_store + .get_compact_header_data(tip_hash) + .map(|tip| tip.daa_score) + .map_err(|_| ReturnAddressError::NotFound(format!("Did not find compact header data for tip hash {}", tip_hash)))?; + + let mut low_index = tip_index.saturating_sub(tip_daa_score.saturating_sub(target_daa_score)).max(source_index); + let mut high_index = tip_index; + + let matching_chain_block_hash = loop { + // Binary search for the chain block that matches the target_daa_score + // 0. Get the mid point index + let mid = low_index + (high_index - low_index) / 2; + + // 1. Get the chain block hash at that index. Error if we don't find a hash at an index + let hash = sc_read.get_by_index(mid).map_err(|_| { + trace!("Did not find a hash at index {}", mid); + ReturnAddressError::NotFound(format!("Did not find a hash at index {}", mid)) + })?; + + // 2. Get the compact header so we have access to the daa_score. Error if we + let compact_header = self.headers_store.get_compact_header_data(hash).map_err(|_| { + trace!("Did not find a compact header with hash {}", hash); + ReturnAddressError::NotFound(format!("Did not find a compact header with hash {}", hash)) + })?; + + // 3. Compare block daa score to our target + match compact_header.daa_score.cmp(&target_daa_score) { + cmp::Ordering::Equal => { + // We found the chain block we need + break hash; + } + cmp::Ordering::Greater => { + high_index = mid - 1; + } + cmp::Ordering::Less => { + low_index = mid + 1; + } + } + + if low_index > high_index { + return Err(ReturnAddressError::NoTxAtScore); + } + }; + + let acceptance_data = self.acceptance_data_store.get(matching_chain_block_hash).map_err(|_| { + ReturnAddressError::NotFound(format!("Did not find acceptance data for chain block {}", matching_chain_block_hash)) + })?; + + Ok((matching_chain_block_hash, acceptance_data)) + } + + fn find_tx_acceptance_data_and_index_from_block_acceptance( + &self, + tx_id: Hash, + block_acceptance_data: Arc>, + ) -> Option<(usize, MergesetBlockAcceptanceData)> { + block_acceptance_data.iter().find_map(|mbad| { + let tx_arr_index = mbad + .accepted_transactions + .iter() + .find_map(|tx| (tx.transaction_id == tx_id).then_some(tx.index_within_block as usize)); + tx_arr_index.map(|index| (index, mbad.clone())) + }) + } + pub fn are_pruning_points_violating_finality(&self, pp_list: PruningPointsList) -> bool { // Ideally we would want to check if the last known pruning point has the finality point // in its chain, but in some cases it's impossible: let `lkp` be the last known pruning diff --git a/rpc/core/src/api/ops.rs b/rpc/core/src/api/ops.rs index 26ca356eb0..4541ddc56d 100644 --- a/rpc/core/src/api/ops.rs +++ b/rpc/core/src/api/ops.rs @@ -136,6 +136,8 @@ pub enum RpcApiOps { GetFeeEstimateExperimental = 148, /// Block color determination by iterating DAG. GetCurrentBlockColor = 149, + /// Get UTXO Return Addresses + GetUtxoReturnAddress = 150, } impl RpcApiOps { diff --git a/rpc/core/src/api/rpc.rs b/rpc/core/src/api/rpc.rs index cadc9e00cd..d9011c167e 100644 --- a/rpc/core/src/api/rpc.rs +++ b/rpc/core/src/api/rpc.rs @@ -438,6 +438,18 @@ pub trait RpcApi: Sync + Send + AnySync { request: GetDaaScoreTimestampEstimateRequest, ) -> RpcResult; + async fn get_utxo_return_address(&self, txid: RpcHash, accepting_block_daa_score: u64) -> RpcResult { + Ok(self + .get_utxo_return_address_call(None, GetUtxoReturnAddressRequest { txid, accepting_block_daa_score }) + .await? + .return_address) + } + async fn get_utxo_return_address_call( + &self, + _connection: Option<&DynRpcConnection>, + request: GetUtxoReturnAddressRequest, + ) -> RpcResult; + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Fee estimation API diff --git a/rpc/core/src/error.rs b/rpc/core/src/error.rs index 0e2bfee225..1f82c40d22 100644 --- a/rpc/core/src/error.rs +++ b/rpc/core/src/error.rs @@ -2,7 +2,7 @@ //! [`RpcError`] enum used by RPC primitives. //! -use kaspa_consensus_core::{subnets::SubnetworkConversionError, tx::TransactionId}; +use kaspa_consensus_core::{return_address::ReturnAddressError, subnets::SubnetworkConversionError, tx::TransactionId}; use kaspa_utils::networking::IpAddress; use std::{net::AddrParseError, num::TryFromIntError}; use thiserror::Error; @@ -134,6 +134,9 @@ pub enum RpcError { #[error(transparent)] ConsensusClient(#[from] kaspa_consensus_client::error::Error), + + #[error("utxo return address could not be found -> {0}")] + UtxoReturnAddressNotFound(ReturnAddressError), } impl From for RpcError { diff --git a/rpc/core/src/model/message.rs b/rpc/core/src/model/message.rs index ba8d6abf76..cb663c394a 100644 --- a/rpc/core/src/model/message.rs +++ b/rpc/core/src/model/message.rs @@ -2666,6 +2666,69 @@ impl Deserializer for GetCurrentBlockColorResponse { } } +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetUtxoReturnAddressRequest { + pub txid: RpcHash, + pub accepting_block_daa_score: u64, +} + +impl GetUtxoReturnAddressRequest { + pub fn new(txid: RpcHash, accepting_block_daa_score: u64) -> Self { + Self { txid, accepting_block_daa_score } + } +} + +impl Serializer for GetUtxoReturnAddressRequest { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcHash, &self.txid, writer)?; + store!(u64, &self.accepting_block_daa_score, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetUtxoReturnAddressRequest { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let txid = load!(RpcHash, reader)?; + let accepting_block_daa_score = load!(u64, reader)?; + + Ok(Self { txid, accepting_block_daa_score }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetUtxoReturnAddressResponse { + pub return_address: RpcAddress, +} + +impl GetUtxoReturnAddressResponse { + pub fn new(return_address: RpcAddress) -> Self { + Self { return_address } + } +} + +impl Serializer for GetUtxoReturnAddressResponse { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcAddress, &self.return_address, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetUtxoReturnAddressResponse { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let return_address = load!(RpcAddress, reader)?; + + Ok(Self { return_address }) + } +} + // ---------------------------------------------------------------------------- // Subscriptions & notifications // ---------------------------------------------------------------------------- diff --git a/rpc/grpc/client/src/lib.rs b/rpc/grpc/client/src/lib.rs index b7e53bb5e1..04e37b5fe6 100644 --- a/rpc/grpc/client/src/lib.rs +++ b/rpc/grpc/client/src/lib.rs @@ -276,6 +276,7 @@ impl RpcApi for GrpcClient { route!(get_fee_estimate_call, GetFeeEstimate); route!(get_fee_estimate_experimental_call, GetFeeEstimateExperimental); route!(get_current_block_color_call, GetCurrentBlockColor); + route!(get_utxo_return_address_call, GetUtxoReturnAddress); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Notification API diff --git a/rpc/grpc/core/Cargo.toml b/rpc/grpc/core/Cargo.toml index 2edc10b600..61734df9e3 100644 --- a/rpc/grpc/core/Cargo.toml +++ b/rpc/grpc/core/Cargo.toml @@ -10,6 +10,7 @@ license.workspace = true repository.workspace = true [dependencies] +kaspa-addresses.workspace = true kaspa-consensus-core.workspace = true kaspa-core.workspace = true kaspa-notify.workspace = true diff --git a/rpc/grpc/core/proto/messages.proto b/rpc/grpc/core/proto/messages.proto index 2d6310d9e3..ccb2798b67 100644 --- a/rpc/grpc/core/proto/messages.proto +++ b/rpc/grpc/core/proto/messages.proto @@ -65,6 +65,7 @@ message KaspadRequest { GetFeeEstimateRequestMessage getFeeEstimateRequest = 1106; GetFeeEstimateExperimentalRequestMessage getFeeEstimateExperimentalRequest = 1108; GetCurrentBlockColorRequestMessage getCurrentBlockColorRequest = 1110; + GetUtxoReturnAddressRequestMessage GetUtxoReturnAddressRequest = 1112; } } @@ -130,6 +131,7 @@ message KaspadResponse { GetFeeEstimateResponseMessage getFeeEstimateResponse = 1107; GetFeeEstimateExperimentalResponseMessage getFeeEstimateExperimentalResponse = 1109; GetCurrentBlockColorResponseMessage getCurrentBlockColorResponse = 1111; + GetUtxoReturnAddressResponseMessage GetUtxoReturnAddressResponse = 1113; } } diff --git a/rpc/grpc/core/proto/rpc.proto b/rpc/grpc/core/proto/rpc.proto index e218681b65..4c36150ad3 100644 --- a/rpc/grpc/core/proto/rpc.proto +++ b/rpc/grpc/core/proto/rpc.proto @@ -904,7 +904,7 @@ message GetDaaScoreTimestampEstimateRequestMessage { repeated uint64 daaScores = 1; } -message GetDaaScoreTimestampEstimateResponseMessage{ +message GetDaaScoreTimestampEstimateResponseMessage { repeated uint64 timestamps = 1; RPCError error = 1000; } @@ -974,3 +974,13 @@ message GetCurrentBlockColorResponseMessage { RPCError error = 1000; } + +message GetUtxoReturnAddressRequestMessage { + string txid = 1; + uint64 accepting_block_daa_score = 2; +} + +message GetUtxoReturnAddressResponseMessage { + string return_address = 1; + RPCError error = 1000; +} diff --git a/rpc/grpc/core/src/convert/kaspad.rs b/rpc/grpc/core/src/convert/kaspad.rs index c3411545cc..7243fd401a 100644 --- a/rpc/grpc/core/src/convert/kaspad.rs +++ b/rpc/grpc/core/src/convert/kaspad.rs @@ -63,6 +63,7 @@ pub mod kaspad_request_convert { impl_into_kaspad_request!(GetFeeEstimate); impl_into_kaspad_request!(GetFeeEstimateExperimental); impl_into_kaspad_request!(GetCurrentBlockColor); + impl_into_kaspad_request!(GetUtxoReturnAddress); impl_into_kaspad_request!(NotifyBlockAdded); impl_into_kaspad_request!(NotifyNewBlockTemplate); @@ -200,6 +201,7 @@ pub mod kaspad_response_convert { impl_into_kaspad_response!(GetFeeEstimate); impl_into_kaspad_response!(GetFeeEstimateExperimental); impl_into_kaspad_response!(GetCurrentBlockColor); + impl_into_kaspad_response!(GetUtxoReturnAddress); impl_into_kaspad_notify_response!(NotifyBlockAdded); impl_into_kaspad_notify_response!(NotifyNewBlockTemplate); diff --git a/rpc/grpc/core/src/convert/message.rs b/rpc/grpc/core/src/convert/message.rs index 67ac60650c..c92e824ed0 100644 --- a/rpc/grpc/core/src/convert/message.rs +++ b/rpc/grpc/core/src/convert/message.rs @@ -19,7 +19,8 @@ //! The SubmitBlockResponse is a notable exception to this general rule. use crate::protowire::{self, submit_block_response_message::RejectReason}; -use kaspa_consensus_core::network::NetworkId; +use kaspa_addresses::Address; +use kaspa_consensus_core::{network::NetworkId, Hash}; use kaspa_core::debug; use kaspa_notify::subscription::Command; use kaspa_rpc_core::{ @@ -430,6 +431,16 @@ from!(item: RpcResult<&kaspa_rpc_core::GetCurrentBlockColorResponse>, protowire: Self { blue: item.blue, error: None } }); +from!(item: &kaspa_rpc_core::GetUtxoReturnAddressRequest, protowire::GetUtxoReturnAddressRequestMessage, { + Self { + txid: item.txid.to_string(), + accepting_block_daa_score: item.accepting_block_daa_score + } +}); +from!(item: RpcResult<&kaspa_rpc_core::GetUtxoReturnAddressResponse>, protowire::GetUtxoReturnAddressResponseMessage, { + Self { return_address: item.return_address.address_to_string(), error: None } +}); + from!(&kaspa_rpc_core::PingRequest, protowire::PingRequestMessage); from!(RpcResult<&kaspa_rpc_core::PingResponse>, protowire::PingResponseMessage); @@ -916,6 +927,15 @@ try_from!(item: &protowire::GetCurrentBlockColorResponseMessage, RpcResult, { + Self { return_address: Address::try_from(item.return_address.clone())? } +}); try_from!(&protowire::PingRequestMessage, kaspa_rpc_core::PingRequest); try_from!(&protowire::PingResponseMessage, RpcResult); diff --git a/rpc/grpc/core/src/ops.rs b/rpc/grpc/core/src/ops.rs index f3bc12c829..223774c74c 100644 --- a/rpc/grpc/core/src/ops.rs +++ b/rpc/grpc/core/src/ops.rs @@ -87,6 +87,7 @@ pub enum KaspadPayloadOps { GetFeeEstimate, GetFeeEstimateExperimental, GetCurrentBlockColor, + GetUtxoReturnAddress, // Subscription commands for starting/stopping notifications NotifyBlockAdded, diff --git a/rpc/grpc/server/src/request_handler/factory.rs b/rpc/grpc/server/src/request_handler/factory.rs index b6a5b4476f..9fec86e476 100644 --- a/rpc/grpc/server/src/request_handler/factory.rs +++ b/rpc/grpc/server/src/request_handler/factory.rs @@ -81,6 +81,7 @@ impl Factory { GetFeeEstimate, GetFeeEstimateExperimental, GetCurrentBlockColor, + GetUtxoReturnAddress, NotifyBlockAdded, NotifyNewBlockTemplate, NotifyFinalityConflict, diff --git a/rpc/grpc/server/src/tests/rpc_core_mock.rs b/rpc/grpc/server/src/tests/rpc_core_mock.rs index dd6de46d2a..ada801850e 100644 --- a/rpc/grpc/server/src/tests/rpc_core_mock.rs +++ b/rpc/grpc/server/src/tests/rpc_core_mock.rs @@ -362,6 +362,14 @@ impl RpcApi for RpcCoreMock { Err(RpcError::NotImplemented) } + async fn get_utxo_return_address_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetUtxoReturnAddressRequest, + ) -> RpcResult { + Err(RpcError::NotImplemented) + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Notification API diff --git a/rpc/service/src/service.rs b/rpc/service/src/service.rs index d75ff770b0..94b4b8f9cd 100644 --- a/rpc/service/src/service.rs +++ b/rpc/service/src/service.rs @@ -794,6 +794,20 @@ NOTE: This error usually indicates an RPC conversion error between the node and } } + async fn get_utxo_return_address_call( + &self, + _connection: Option<&DynRpcConnection>, + request: GetUtxoReturnAddressRequest, + ) -> RpcResult { + let session = self.consensus_manager.consensus().session().await; + + // Convert a SPK to an Address + match session.async_get_utxo_return_script_public_key(request.txid, request.accepting_block_daa_score).await { + Ok(return_address) => return Ok(GetUtxoReturnAddressResponse { return_address }), + Err(error) => return Err(RpcError::UtxoReturnAddressNotFound(error)), + }; + } + async fn ping_call(&self, _connection: Option<&DynRpcConnection>, _: PingRequest) -> RpcResult { Ok(PingResponse {}) } diff --git a/rpc/wrpc/client/src/client.rs b/rpc/wrpc/client/src/client.rs index 3ac04fa984..f22bcf6255 100644 --- a/rpc/wrpc/client/src/client.rs +++ b/rpc/wrpc/client/src/client.rs @@ -644,6 +644,7 @@ impl RpcApi for KaspaRpcClient { GetSubnetwork, GetSyncStatus, GetSystemInfo, + GetUtxoReturnAddress, GetUtxosByAddresses, GetVirtualChainFromBlock, ResolveFinalityConflict, diff --git a/rpc/wrpc/server/src/router.rs b/rpc/wrpc/server/src/router.rs index 513f044141..b4c74a3374 100644 --- a/rpc/wrpc/server/src/router.rs +++ b/rpc/wrpc/server/src/router.rs @@ -47,6 +47,8 @@ impl Router { GetCurrentBlockColor, GetCoinSupply, GetConnectedPeerInfo, + GetDaaScoreTimestampEstimate, + GetUtxoReturnAddress, GetCurrentNetwork, GetDaaScoreTimestampEstimate, GetFeeEstimate, diff --git a/testing/integration/src/daemon_integration_tests.rs b/testing/integration/src/daemon_integration_tests.rs index 460cf049c3..25471cab13 100644 --- a/testing/integration/src/daemon_integration_tests.rs +++ b/testing/integration/src/daemon_integration_tests.rs @@ -274,7 +274,8 @@ async fn daemon_utxos_propagation_test() { clients.iter().for_each(|x| x.utxos_changed_listener().unwrap().drain()); clients.iter().for_each(|x| x.virtual_daa_score_changed_listener().unwrap().drain()); - // Spend some coins + // Spend some coins - sending funds from miner address to user address + // The transaction here is later used to verify utxo return address RPC const NUMBER_INPUTS: u64 = 2; const NUMBER_OUTPUTS: u64 = 2; const TX_AMOUNT: u64 = SIMNET_PARAMS.pre_deflationary_phase_base_subsidy * (NUMBER_INPUTS * 5 - 1) / 5; @@ -324,6 +325,23 @@ async fn daemon_utxos_propagation_test() { assert_eq!(user_balance, TX_AMOUNT); } + // UTXO Return Address Test + // Mine another block to accept the transactions from the previous block + // The tx above is sending from miner address to user address + mine_block(blank_address.clone(), &rpc_client1, &clients).await; + let new_utxos = rpc_client1.get_utxos_by_addresses(vec![user_address]).await.unwrap(); + let new_utxo = new_utxos + .iter() + .find(|utxo| utxo.outpoint.transaction_id == transaction.id()) + .expect("Did not find a utxo for the tx we just created but expected to"); + + let utxo_return_address = rpc_client1 + .get_utxo_return_address(new_utxo.outpoint.transaction_id, new_utxo.utxo_entry.block_daa_score) + .await + .expect("We just created the tx and utxo here"); + + assert_eq!(miner_address, utxo_return_address); + // Terminate multi-listener clients for x in clients.iter() { x.disconnect().await.unwrap(); diff --git a/testing/integration/src/rpc_tests.rs b/testing/integration/src/rpc_tests.rs index 3c4df601b3..3ca619423a 100644 --- a/testing/integration/src/rpc_tests.rs +++ b/testing/integration/src/rpc_tests.rs @@ -656,6 +656,23 @@ async fn sanity_test() { }) } + KaspadPayloadOps::GetUtxoReturnAddress => { + let rpc_client = client.clone(); + tst!(op, { + let results = rpc_client.get_utxo_return_address(RpcHash::from_bytes([0; 32]), 1000).await; + + assert!(results.is_err_and(|err| { + match err { + kaspa_rpc_core::RpcError::General(msg) => { + info!("Expected error message: {}", msg); + true + } + _ => false, + } + })); + }) + } + KaspadPayloadOps::NotifyBlockAdded => { let rpc_client = client.clone(); let id = listener_id; diff --git a/wallet/core/src/tests/rpc_core_mock.rs b/wallet/core/src/tests/rpc_core_mock.rs index 4d10cdd9b1..529fffffe8 100644 --- a/wallet/core/src/tests/rpc_core_mock.rs +++ b/wallet/core/src/tests/rpc_core_mock.rs @@ -379,6 +379,14 @@ impl RpcApi for RpcCoreMock { Err(RpcError::NotImplemented) } + async fn get_utxo_return_address_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetUtxoReturnAddressRequest, + ) -> RpcResult { + Err(RpcError::NotImplemented) + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Notification API