Compare commits

..

3 commits

Author SHA1 Message Date
hinto.janai
d4b30333bb
most of /send_raw_transaction
Some checks failed
Deny / audit (push) Has been cancelled
2024-12-13 16:43:39 -05:00
hinto.janai
c38daa4497
/get_transactions hex 2024-12-13 15:36:11 -05:00
hinto.janai
ab4822c660
!! 2024-12-13 15:16:26 -05:00
14 changed files with 245 additions and 28 deletions

1
Cargo.lock generated
View file

@ -1142,6 +1142,7 @@ dependencies = [
name = "cuprate-types"
version = "0.0.0"
dependencies = [
"bitflags 2.6.0",
"bytes",
"cfg-if",
"cuprate-epee-encoding",

View file

@ -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)]

View file

@ -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)),
})
}

View file

@ -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)),
})
}
@ -142,19 +141,34 @@ async fn get_transactions(
txpool::txs_by_hash(&mut state.txpool_read, missed_txs, include_sensitive_txs).await?
};
let (txs, txs_as_json) = {
let (txs, txs_as_hex, txs_as_json) = {
// Prepare the final JSON output.
let len = txs_in_blockchain.len() + txs_in_pool.len();
let mut txs = Vec::with_capacity(len);
let mut txs_as_hex = Vec::with_capacity(len);
let mut txs_as_json = Vec::with_capacity(if request.decode_as_json { len } else { 0 });
// Map all blockchain transactions.
for tx in txs_in_blockchain {
let tx_hash = Hex(tx.tx_hash);
let pruned_as_hex = hex::encode(tx.pruned_blob);
let prunable_as_hex = hex::encode(tx.prunable_blob);
let prunable_hash = Hex(tx.prunable_hash);
let (pruned_as_hex, prunable_as_hex) = if tx.pruned_blob.is_empty() {
(String::new(), String::new())
} else {
(hex::encode(tx.pruned_blob), hex::encode(tx.prunable_blob))
};
let as_hex = if pruned_as_hex.is_empty() {
// `monerod` will insert a `""` into the `txs_as_hex` array for pruned transactions.
// curl http://127.0.0.1:18081/get_transactions -d '{"txs_hashes":["4c8b98753d1577d225a497a50f453827cff3aa023a4add60ec4ce4f923f75de8"]}' -H 'Content-Type: application/json'
String::new()
} else {
hex::encode(&tx.tx_blob)
};
txs_as_hex.push(as_hex.clone());
let as_json = if request.decode_as_json {
let tx = Transaction::read(&mut tx.tx_blob.as_slice())?;
let json_type = cuprate_types::json::tx::Transaction::from(tx);
@ -174,7 +188,7 @@ async fn get_transactions(
};
let tx = TxEntry {
as_hex: String::new(),
as_hex,
as_json,
double_spend_seen: false,
tx_hash,
@ -200,11 +214,13 @@ async fn get_transactions(
let tx_hash = Hex(tx_hash);
let tx = Transaction::read(&mut tx_blob.as_slice())?;
// TODO: pruned data.
let pruned_as_hex = String::new();
let prunable_as_hex = String::new();
let prunable_hash = Hex([0; 32]);
let as_hex = hex::encode(tx_blob);
txs_as_hex.push(as_hex.clone());
let as_json = if request.decode_as_json {
let json_type = cuprate_types::json::tx::Transaction::from(tx);
let json = serde_json::to_string(&json_type).unwrap();
@ -221,7 +237,7 @@ async fn get_transactions(
};
let tx = TxEntry {
as_hex: String::new(),
as_hex,
as_json,
double_spend_seen,
tx_hash,
@ -234,12 +250,12 @@ async fn get_transactions(
txs.push(tx);
}
(txs, txs_as_json)
(txs, txs_as_hex, txs_as_json)
};
Ok(GetTransactionsResponse {
base: AccessResponseBase::OK,
txs_as_hex: vec![],
txs_as_hex,
txs_as_json,
missed_tx,
txs,
@ -312,13 +328,134 @@ async fn is_key_image_spent(
/// <https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/rpc/core_rpc_server.cpp#L1307-L1411>
async fn send_raw_transaction(
state: CupratedRpcHandler,
mut state: CupratedRpcHandler,
request: SendRawTransactionRequest,
) -> Result<SendRawTransactionResponse, Error> {
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.
//
// <https://github.com/monero-project/monero/blob/893916ad091a92e765ce3241b94e706ad012b62a/src/cryptonote_core/tx_sanity_check.cpp#L42>
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);
}
// <https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/rpc/core_rpc_server.cpp#L124>
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)
}
/// <https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/rpc/core_rpc_server.cpp#L1413-L1462>

View file

@ -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<u64, Error> {
let BlockchainResponse::TotalRctOutputs(total_rct_outputs) = blockchain_read
.ready()
.await?
.call(BlockchainReadRequest::TotalRctOutputs)
.await?
else {
unreachable!();
};
Ok(total_rct_outputs)
}

View file

@ -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<TxRelayChecks, Error> {
Ok(todo!())
}

View file

@ -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.

View file

@ -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,
};

View file

@ -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<String> 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<str> 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,

View file

@ -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::<RctOutputs>(&tx_ro)?.len()?;
Ok(BlockchainResponse::TotalRctOutputs(len))
}

View file

@ -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 }

View file

@ -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.
///

View file

@ -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

View file

@ -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 {