diff --git a/Cargo.lock b/Cargo.lock index 661e5f23..81fac819 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1142,6 +1142,7 @@ dependencies = [ name = "cuprate-types" version = "0.0.0" dependencies = [ + "bitflags 2.6.0", "bytes", "cfg-if", "cuprate-epee-encoding", diff --git a/binaries/cuprated/src/constants.rs b/binaries/cuprated/src/constants.rs index 057e8bd0..2685f663 100644 --- a/binaries/cuprated/src/constants.rs +++ b/binaries/cuprated/src/constants.rs @@ -18,6 +18,9 @@ pub const VERSION_BUILD: &str = if cfg!(debug_assertions) { pub const PANIC_CRITICAL_SERVICE_ERROR: &str = "A service critical to Cuprate's function returned an unexpected error."; +/// The error message returned when an unsupported RPC call is requested. +pub const UNSUPPORTED_RPC_CALL: &str = "This RPC call is not supported by Cuprate."; + pub const EXAMPLE_CONFIG: &str = include_str!("../Cuprated.toml"); #[cfg(test)] diff --git a/binaries/cuprated/src/rpc/json.rs b/binaries/cuprated/src/rpc/json.rs index e704ce0e..8dff6e76 100644 --- a/binaries/cuprated/src/rpc/json.rs +++ b/binaries/cuprated/src/rpc/json.rs @@ -58,7 +58,7 @@ use cuprate_types::{ }; use crate::{ - constants::VERSION_BUILD, + constants::{UNSUPPORTED_RPC_CALL, VERSION_BUILD}, rpc::{ helper, request::{address_book, blockchain, blockchain_context, blockchain_manager, txpool}, @@ -122,9 +122,10 @@ pub(super) async fn map_request( Req::GetMinerData(r) => Resp::GetMinerData(get_miner_data(state, r).await?), Req::PruneBlockchain(r) => Resp::PruneBlockchain(prune_blockchain(state, r).await?), Req::CalcPow(r) => Resp::CalcPow(calc_pow(state, r).await?), - Req::FlushCache(r) => Resp::FlushCache(flush_cache(state, r).await?), Req::AddAuxPow(r) => Resp::AddAuxPow(add_aux_pow(state, r).await?), - Req::GetTxIdsLoose(r) => Resp::GetTxIdsLoose(get_tx_ids_loose(state, r).await?), + + // Unsupported RPC calls. + Req::GetTxIdsLoose(_) | Req::FlushCache(_) => return Err(anyhow!(UNSUPPORTED_RPC_CALL)), }) } diff --git a/binaries/cuprated/src/rpc/other.rs b/binaries/cuprated/src/rpc/other.rs index 6ef0a75b..a88633c7 100644 --- a/binaries/cuprated/src/rpc/other.rs +++ b/binaries/cuprated/src/rpc/other.rs @@ -34,15 +34,16 @@ use cuprate_rpc_types::{ }; use cuprate_types::{ rpc::{KeyImageSpentStatus, OutKey, PoolInfo, PoolTxInfo}, - TxInPool, + TxInPool, TxRelayChecks, }; -use monero_serai::transaction::Transaction; +use monero_serai::transaction::{Input, Transaction}; use crate::{ - rpc::CupratedRpcHandler, + constants::UNSUPPORTED_RPC_CALL, rpc::{ helper, request::{blockchain, blockchain_context, blockchain_manager, txpool}, + CupratedRpcHandler, }, }; @@ -84,7 +85,6 @@ pub(super) async fn map_request( Req::InPeers(r) => Resp::InPeers(in_peers(state, r).await?), Req::GetNetStats(r) => Resp::GetNetStats(get_net_stats(state, r).await?), Req::GetOuts(r) => Resp::GetOuts(get_outs(state, r).await?), - Req::Update(r) => Resp::Update(update(state, r).await?), Req::PopBlocks(r) => Resp::PopBlocks(pop_blocks(state, r).await?), Req::GetTransactionPoolHashes(r) => { Resp::GetTransactionPoolHashes(get_transaction_pool_hashes(state, r).await?) @@ -92,12 +92,11 @@ pub(super) async fn map_request( Req::GetPublicNodes(r) => Resp::GetPublicNodes(get_public_nodes(state, r).await?), // Unsupported requests. - Req::StartMining(_) + Req::Update(_) + | Req::StartMining(_) | Req::StopMining(_) | Req::MiningStatus(_) - | Req::SetLogHashRate(_) => { - return Err(anyhow!("Mining RPC calls are not supported by Cuprate")) - } + | Req::SetLogHashRate(_) => return Err(anyhow!(UNSUPPORTED_RPC_CALL)), }) } @@ -329,13 +328,134 @@ async fn is_key_image_spent( /// async fn send_raw_transaction( - state: CupratedRpcHandler, + mut state: CupratedRpcHandler, request: SendRawTransactionRequest, ) -> Result { - Ok(SendRawTransactionResponse { + let mut resp = SendRawTransactionResponse { base: AccessResponseBase::OK, - ..todo!() - }) + double_spend: false, + fee_too_low: false, + invalid_input: false, + invalid_output: false, + low_mixin: false, + nonzero_unlock_time: false, + not_relayed: request.do_not_relay, + overspend: false, + reason: String::new(), + sanity_check_failed: false, + too_big: false, + too_few_outputs: false, + tx_extra_too_big: false, + }; + + let tx = { + let blob = hex::decode(request.tx_as_hex)?; + Transaction::read(&mut blob.as_slice())? + }; + + if request.do_sanity_checks { + // FIXME: these checks could be defined elsewhere. + // + // + fn tx_sanity_check(tx: &Transaction, rct_outs_available: u64) -> Result<(), &'static str> { + let Some(input) = tx.prefix().inputs.get(0) else { + return Err("No inputs"); + }; + + let mut rct_indices = BTreeSet::new(); + let n_indices: usize = 0; + + for input in tx.prefix().inputs { + match input { + Input::Gen(_) => return Err("Transaction is coinbase"), + Input::ToKey { + amount, + key_offsets, + key_image, + } => { + let Some(amount) = amount else { + continue; + }; + + n_indices += key_offsets.len(); + let absolute = todo!(); + rct_indices.extend(absolute); + } + } + } + + if n_indices <= 10 { + return Ok(()); + } + + if rct_outs_available < 10_000 { + return Ok(()); + } + + let rct_indices_len = rct_indices.len(); + if rct_indices_len < n_indices * 8 / 10 { + return Err("amount of unique indices is too low (amount of rct indices is {rct_indices_len} out of total {n_indices} indices."); + } + + let offsets = Vec::with_capacity(rct_indices_len); + let median = todo!(); + if median < rct_outs_available * 6 / 10 { + return Err("median offset index is too low (median is {median} out of total {rct_outs_available} offsets). Transactions should contain a higher fraction of recent outputs."); + } + + Ok(()) + } + + let rct_outs_available = blockchain::total_rct_outputs(&mut state.blockchain_read).await?; + + if let Err(e) = tx_sanity_check(&tx, rct_outs_available) { + resp.base.response_base.status = Status::Failed; + resp.reason.push_str(&format!("Sanity check failed: {e}")); + resp.sanity_check_failed = true; + return Ok(resp); + } + } + + let tx_relay_checks = + txpool::check_maybe_relay_local(&mut state.txpool_manager, tx, !request.do_not_relay) + .await?; + + if tx_relay_checks.is_empty() { + return Ok(resp); + } + + // + fn add_reason(reasons: &mut String, reason: &'static str) { + if !reasons.is_empty() { + reasons.push_str(", "); + } + reasons.push_str(reason); + } + + let mut reasons = String::new(); + + #[rustfmt::skip] + let array = [ + (&mut resp.double_spend, TxRelayChecks::DOUBLE_SPEND, "double spend"), + (&mut resp.fee_too_low, TxRelayChecks::FEE_TOO_LOW, "fee too low"), + (&mut resp.invalid_input, TxRelayChecks::INVALID_INPUT, "invalid input"), + (&mut resp.invalid_output, TxRelayChecks::INVALID_OUTPUT, "invalid output"), + (&mut resp.low_mixin, TxRelayChecks::LOW_MIXIN, "bad ring size"), + (&mut resp.nonzero_unlock_time, TxRelayChecks::NONZERO_UNLOCK_TIME, "tx unlock time is not zero"), + (&mut resp.overspend, TxRelayChecks::OVERSPEND, "overspend"), + (&mut resp.too_big, TxRelayChecks::TOO_BIG, "too big"), + (&mut resp.too_few_outputs, TxRelayChecks::TOO_FEW_OUTPUTS, "too few outputs"), + (&mut resp.tx_extra_too_big, TxRelayChecks::TX_EXTRA_TOO_BIG, "tx-extra too big"), + ]; + + for (field, flag, reason) in array { + if tx_relay_checks.contains(flag) { + *field = true; + add_reason(&mut reasons, reason); + } + } + + Ok(resp) } /// diff --git a/binaries/cuprated/src/rpc/request/blockchain.rs b/binaries/cuprated/src/rpc/request/blockchain.rs index 1c0f4c07..e5c470f9 100644 --- a/binaries/cuprated/src/rpc/request/blockchain.rs +++ b/binaries/cuprated/src/rpc/request/blockchain.rs @@ -394,3 +394,19 @@ pub(crate) async fn transactions( Ok((txs, missed_txs)) } + +/// [`BlockchainReadRequest::TotalRctOutputs`]. +pub(crate) async fn total_rct_outputs( + blockchain_read: &mut BlockchainReadHandle, +) -> Result { + let BlockchainResponse::TotalRctOutputs(total_rct_outputs) = blockchain_read + .ready() + .await? + .call(BlockchainReadRequest::TotalRctOutputs) + .await? + else { + unreachable!(); + }; + + Ok(total_rct_outputs) +} diff --git a/binaries/cuprated/src/rpc/request/txpool.rs b/binaries/cuprated/src/rpc/request/txpool.rs index f6ff2793..95d49ef1 100644 --- a/binaries/cuprated/src/rpc/request/txpool.rs +++ b/binaries/cuprated/src/rpc/request/txpool.rs @@ -3,6 +3,7 @@ use std::{convert::Infallible, num::NonZero}; use anyhow::{anyhow, Error}; +use monero_serai::transaction::Transaction; use tower::{Service, ServiceExt}; use cuprate_helper::cast::usize_to_u64; @@ -15,7 +16,7 @@ use cuprate_txpool::{ }; use cuprate_types::{ rpc::{PoolInfo, PoolInfoFull, PoolInfoIncremental, PoolTxInfo}, - TxInPool, + TxInPool, TxRelayChecks, }; // FIXME: use `anyhow::Error` over `tower::BoxError` in txpool. @@ -145,3 +146,12 @@ pub(crate) async fn relay( todo!(); Ok(()) } + +/// TODO +pub(crate) async fn check_maybe_relay_local( + txpool_manager: &mut Infallible, + tx: Transaction, + relay: bool, +) -> Result { + Ok(todo!()) +} diff --git a/rpc/types/src/constants.rs b/rpc/types/src/constants.rs index a6a68c32..ce601d79 100644 --- a/rpc/types/src/constants.rs +++ b/rpc/types/src/constants.rs @@ -36,6 +36,9 @@ pub const CORE_RPC_STATUS_NOT_MINING: &str = "NOT MINING"; #[doc = monero_definition_link!("cc73fe71162d564ffda8e549b79a350bca53c454", "/rpc/core_rpc_server_commands_defs.h", 81)] pub const CORE_RPC_STATUS_PAYMENT_REQUIRED: &str = "PAYMENT REQUIRED"; +/// Not defined in `monerod` although used frequently. +pub const CORE_RPC_STATUS_FAILED: &str = "Failed"; + //---------------------------------------------------------------------------------------------------- Versions #[doc = monero_definition_link!("cc73fe71162d564ffda8e549b79a350bca53c454", "/rpc/core_rpc_server_commands_defs.h", 90)] /// RPC major version. diff --git a/rpc/types/src/lib.rs b/rpc/types/src/lib.rs index 353ca001..ea2077aa 100644 --- a/rpc/types/src/lib.rs +++ b/rpc/types/src/lib.rs @@ -24,7 +24,7 @@ pub mod misc; pub mod other; pub use constants::{ - CORE_RPC_STATUS_BUSY, CORE_RPC_STATUS_NOT_MINING, CORE_RPC_STATUS_OK, + CORE_RPC_STATUS_BUSY, CORE_RPC_STATUS_FAILED, CORE_RPC_STATUS_NOT_MINING, CORE_RPC_STATUS_OK, CORE_RPC_STATUS_PAYMENT_REQUIRED, CORE_RPC_VERSION, CORE_RPC_VERSION_MAJOR, CORE_RPC_VERSION_MINOR, }; diff --git a/rpc/types/src/misc/status.rs b/rpc/types/src/misc/status.rs index 79725cff..297addee 100644 --- a/rpc/types/src/misc/status.rs +++ b/rpc/types/src/misc/status.rs @@ -13,7 +13,7 @@ use cuprate_epee_encoding::{ }; use crate::constants::{ - CORE_RPC_STATUS_BUSY, CORE_RPC_STATUS_NOT_MINING, CORE_RPC_STATUS_OK, + CORE_RPC_STATUS_BUSY, CORE_RPC_STATUS_FAILED, CORE_RPC_STATUS_NOT_MINING, CORE_RPC_STATUS_OK, CORE_RPC_STATUS_PAYMENT_REQUIRED, }; @@ -40,24 +40,28 @@ use crate::constants::{ /// let other = Status::Other("OTHER".into()); /// /// assert_eq!(to_string(&Status::Ok).unwrap(), r#""OK""#); +/// assert_eq!(to_string(&Status::Failed).unwrap(), r#""Failed""#); /// assert_eq!(to_string(&Status::Busy).unwrap(), r#""BUSY""#); /// assert_eq!(to_string(&Status::NotMining).unwrap(), r#""NOT MINING""#); /// assert_eq!(to_string(&Status::PaymentRequired).unwrap(), r#""PAYMENT REQUIRED""#); /// assert_eq!(to_string(&other).unwrap(), r#""OTHER""#); /// /// assert_eq!(Status::Ok.as_ref(), CORE_RPC_STATUS_OK); +/// assert_eq!(Status::Failed.as_ref(), CORE_RPC_STATUS_FAILED); /// assert_eq!(Status::Busy.as_ref(), CORE_RPC_STATUS_BUSY); /// assert_eq!(Status::NotMining.as_ref(), CORE_RPC_STATUS_NOT_MINING); /// assert_eq!(Status::PaymentRequired.as_ref(), CORE_RPC_STATUS_PAYMENT_REQUIRED); /// assert_eq!(other.as_ref(), "OTHER"); /// /// assert_eq!(format!("{}", Status::Ok), CORE_RPC_STATUS_OK); +/// assert_eq!(format!("{}", Status::Failed), CORE_RPC_STATUS_FAILED); /// assert_eq!(format!("{}", Status::Busy), CORE_RPC_STATUS_BUSY); /// assert_eq!(format!("{}", Status::NotMining), CORE_RPC_STATUS_NOT_MINING); /// assert_eq!(format!("{}", Status::PaymentRequired), CORE_RPC_STATUS_PAYMENT_REQUIRED); /// assert_eq!(format!("{}", other), "OTHER"); /// /// assert_eq!(format!("{:?}", Status::Ok), "Ok"); +/// assert_eq!(format!("{:?}", Status::Failed), "Failed"); /// assert_eq!(format!("{:?}", Status::Busy), "Busy"); /// assert_eq!(format!("{:?}", Status::NotMining), "NotMining"); /// assert_eq!(format!("{:?}", Status::PaymentRequired), "PaymentRequired"); @@ -74,6 +78,10 @@ pub enum Status { #[default] Ok, + /// Generic request failure. + #[cfg_attr(feature = "serde", serde(rename = "Failed"))] + Failed, + /// The daemon is busy, try later; [`CORE_RPC_STATUS_BUSY`]. #[cfg_attr(feature = "serde", serde(rename = "BUSY"))] Busy, @@ -101,6 +109,7 @@ impl From for Status { CORE_RPC_STATUS_BUSY => Self::Busy, CORE_RPC_STATUS_NOT_MINING => Self::NotMining, CORE_RPC_STATUS_PAYMENT_REQUIRED => Self::PaymentRequired, + CORE_RPC_STATUS_FAILED => Self::Failed, _ => Self::Other(s), } } @@ -110,6 +119,7 @@ impl AsRef for Status { fn as_ref(&self) -> &str { match self { Self::Ok => CORE_RPC_STATUS_OK, + Self::Failed => CORE_RPC_STATUS_FAILED, Self::Busy => CORE_RPC_STATUS_BUSY, Self::NotMining => CORE_RPC_STATUS_NOT_MINING, Self::PaymentRequired => CORE_RPC_STATUS_PAYMENT_REQUIRED, diff --git a/storage/blockchain/src/service/read.rs b/storage/blockchain/src/service/read.rs index 3efbb9a2..831dca23 100644 --- a/storage/blockchain/src/service/read.rs +++ b/storage/blockchain/src/service/read.rs @@ -51,7 +51,9 @@ use crate::{ free::{compact_history_genesis_not_included, compact_history_index_to_height_offset}, types::{BlockchainReadHandle, ResponseResult}, }, - tables::{AltBlockHeights, BlockHeights, BlockInfos, OpenTables, Tables, TablesIter}, + tables::{ + AltBlockHeights, BlockHeights, BlockInfos, OpenTables, RctOutputs, Tables, TablesIter, + }, types::{ AltBlockHeight, Amount, AmountIndex, BlockHash, BlockHeight, KeyImage, PreRctOutputId, }, @@ -135,6 +137,7 @@ fn map_request( R::AltChains => alt_chains(env), R::AltChainCount => alt_chain_count(env), R::Transactions { tx_hashes } => transactions(env, tx_hashes), + R::TotalRctOutputs => total_rct_outputs(env), } /* SOMEDAY: post-request handling, run some code for each request? */ @@ -779,3 +782,13 @@ fn transactions(env: &ConcreteEnv, tx_hashes: HashSet<[u8; 32]>) -> ResponseResu missed_txs: todo!(), }) } + +/// [`BlockchainReadRequest::TotalRctOutputs`] +fn total_rct_outputs(env: &ConcreteEnv) -> ResponseResult { + // Single-threaded, no `ThreadLocal` required. + let env_inner = env.env_inner(); + let tx_ro = env_inner.tx_ro()?; + let len = env_inner.open_db_ro::(&tx_ro)?.len()?; + + Ok(BlockchainResponse::TotalRctOutputs(len)) +} diff --git a/types/types/Cargo.toml b/types/types/Cargo.toml index 84901a3f..c2b316de 100644 --- a/types/types/Cargo.toml +++ b/types/types/Cargo.toml @@ -23,6 +23,7 @@ cuprate-helper = { workspace = true, optional = true, features = ["cast"] cuprate-fixed-bytes = { workspace = true, features = ["std", "serde"] } cuprate-hex = { workspace = true, optional = true } +bitflags = { workspace = true } bytes = { workspace = true } cfg-if = { workspace = true } curve25519-dalek = { workspace = true } diff --git a/types/types/src/blockchain.rs b/types/types/src/blockchain.rs index 3ce1efbd..143a7c20 100644 --- a/types/types/src/blockchain.rs +++ b/types/types/src/blockchain.rs @@ -163,6 +163,9 @@ pub enum BlockchainReadRequest { /// TODO Transactions { tx_hashes: HashSet<[u8; 32]> }, + + /// TODO + TotalRctOutputs, } //---------------------------------------------------------------------------------------------------- WriteRequest @@ -358,6 +361,9 @@ pub enum BlockchainResponse { missed_txs: Vec<[u8; 32]>, }, + /// Response to [`BlockchainReadRequest::TotalRctOutputs`]. + TotalRctOutputs(u64), + //------------------------------------------------------ Writes /// A generic Ok response to indicate a request was successfully handled. /// diff --git a/types/types/src/lib.rs b/types/types/src/lib.rs index f4c7d034..c2ac90e0 100644 --- a/types/types/src/lib.rs +++ b/types/types/src/lib.rs @@ -25,7 +25,8 @@ pub use transaction_verification_data::{ }; pub use types::{ AltBlockInformation, BlockTemplate, Chain, ChainId, ExtendedBlockHeader, OutputOnChain, - TxInBlockchain, TxInPool, TxsInBlock, VerifiedBlockInformation, VerifiedTransactionInformation, + TxInBlockchain, TxInPool, TxRelayChecks, TxsInBlock, VerifiedBlockInformation, + VerifiedTransactionInformation, }; //---------------------------------------------------------------------------------------------------- Feature-gated diff --git a/types/types/src/types.rs b/types/types/src/types.rs index 3f899306..8291a252 100644 --- a/types/types/src/types.rs +++ b/types/types/src/types.rs @@ -191,6 +191,21 @@ pub struct TxInPool { pub relayed: bool, } +bitflags::bitflags! { + pub struct TxRelayChecks: u16 { + const DOUBLE_SPEND = 1; + const FEE_TOO_LOW = 1 << 1; + const INVALID_INPUT = 1 << 2; + const INVALID_OUTPUT = 1 << 3; + const LOW_MIXIN = 1 << 4; + const NONZERO_UNLOCK_TIME = 1 << 5; + const OVERSPEND = 1 << 6; + const TOO_BIG = 1 << 7; + const TOO_FEW_OUTPUTS = 1 << 8; + const TX_EXTRA_TOO_BIG = 1 << 9; + } +} + //---------------------------------------------------------------------------------------------------- Tests #[cfg(test)] mod test {