Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cuprated: RPC handlers #355

Open
wants to merge 119 commits into
base: main
Choose a base branch
from
Open

cuprated: RPC handlers #355

wants to merge 119 commits into from

Conversation

hinto-janai
Copy link
Contributor

@hinto-janai hinto-janai commented Dec 5, 2024

What

Implements the {json-rpc, binary, json} handlers in cuprated; adds various types and changes some misc things as needed.

How

See below review.

Copy link
Contributor Author

@hinto-janai hinto-janai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Boog900 I think this mostly needs review on:

  • general direction and broader changes
  • viability of new tower::Service request/response types

I don't think you have to review each handler fn individually in-depth since there will eventually be the testing harness that proves input/output matches monerod. With that said, here's some info:

RPC code structure

Directory Contains
binaries/cuprated/src/rpc/handlers/ The RPC handler functions, shared functions, and helper functions
binaries/cuprated/src/rpc/service/ fn versions of our tower::Services to reduce noise

RPC handlers

Color Meaning
🟢 Ready
🟡 Ready but could be more efficient
🟣 Ready but callstack depends on other things
🟠 Depends on other things
🔴 Waiting on binary strings
🔵 Unsupported (for now)
Unsupported (probably forever)
I think these could be deprecated or have different behavior
JSON-RPC fn Status Details
get_block_count 🟢
get_last_block_header 🟢
get_block_header_by_hash 🟢
get_block_header_by_height 🟢
get_block 🟢
hard_fork_info 🟢
on_get_block_hash 🟢
get_block_headers_range 🟡 Would benefit from a request that allows retrieving a range of (Block, ExtendedBlockHeader)
get_connections 🟣 Waiting on address book Service impl
set_bans 🟣 ^
get_bans 🟣 ^
banned 🟣 ^
get_version 🟣 Waiting on blockchain context Service impl
get_output_histogram 🟣 ^
get_fee_estimate 🟣 ^
calc_pow 🟣 ^
flush_transaction_pool 🟣 Waiting on txpool manager Service impl
relay_tx 🟣 ^
get_coinbase_tx_sum 🟣 Waiting on blockchain Service impl
get_alternate_chains 🟣 ^
sync_info 🟣 Waiting on multiple Service impls
get_miner_data 🟣 Waiting on txpool Service impl
submit_block 🟣 ^
get_info 🟣 Needs access to unimplemented things from various places
generate_blocks 🟣 Needs access to cuprated's Chain and --regtest
add_aux_pow 🟣 Waiting on crypto functions
get_transaction_pool_backlog 🔴
get_output_distribution 🔴
get_tx_ids_loose 🔵 Not implemented in monerod release branch yet
flush_cache cuprated does not need this
prune_blockchain I don't think an always available RPC method is necessary for something that is done once. This could return if the chain is pruned + the pruning seed but not actually prune if that makes things more complex. Pruning itself should be done with the equivalent of --prune-blockchain.
Other JSON fn Status Details
get_height 🟢
get_outs 🟢
is_key_image_spent 🟣 Waiting on txpool Service impl
get_transaction_pool_hashes 🟣 ^
get_transaction_pool 🟣 ^
get_transaction_pool_stats 🟣 ^
save_bc 🟣 Waiting on blockchain manager Service impl
stop_daemon 🟣 ^
pop_blocks 🟣 ^
get_peer_list 🟣 Waiting on address book Service impl
get_public_nodes 🟣 ^
get_alt_blocks_hashes 🟣 Waiting on blockchain Service impl
send_raw_transaction 🟣 Waiting on txpool manager Service impl
get_transactions 🟣 Waiting on JSON representation of Transaction
get_limit 🟠 Waiting on P2P interface
set_limit 🟠 ^
out_peers 🟠 ^
in_peers 🟠 ^
get_net_stats 🟠 ^
set_log_level 🔵 Will use tracing levels
set_log_categories 🔵 Could be re-purposed to use tracing filters
set_bootstrap_daemon 🔵 Needs bootstrap implementation
start_mining cuprated does not mine
stop_mining ^
mining_status ^
set_log_hash_rate ^
update This could return if an update is available and related info but not actually self-update. Software upgrades should be done by the user and/or a package manager.
Binary fn Status Details
get_blocks_by_height 🟢
get_hashes 🟢
get_output_indexes 🟢
get_outs 🟢
get_blocks 🟣 Waiting on txpool Service impl
get_transaction_pool_hashes 🟣 ^
get_output_distribution 🔴 Although this is binary, the internal fn is shared with JSON-RPC's get_output_distribution

Cargo.toml Show resolved Hide resolved
@@ -291,6 +291,7 @@ macro_rules! define_response {
}
) => {
$( #[$attr] )*
#[cfg_attr(feature = "serde", serde(default))] // TODO: link epee field not serializing oddity
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the time being, I'm making response types in cuprate-rpc-types have serde(default) since:

  1. monerod will not serialize fields with empty containers
  2. monerod's response types sometimes change depending on branches

serde(default) is a bit of a blunt tool to use but I think it works for now. I will think about this more when integrating RPC into cuprated and with more tests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I said in the other issue I am ok with this for now, I don't like the idea of this being permanent though

types/types/src/rpc/types.rs Show resolved Hide resolved
rpc/types/src/from.rs Show resolved Hide resolved
rpc/types/src/json.rs Show resolved Hide resolved
rpc/types/src/json.rs Show resolved Hide resolved
helper/src/fmt.rs Show resolved Hide resolved
Comment on lines +177 to 195
let txid = {
let height = u32_to_usize(output.height);
let tx_idx = u64_to_usize(output.tx_idx);
if let Some(hash) = table_block_txs_hashes.get(&height)?.get(tx_idx) {
*hash
} else {
let miner_tx_id = table_block_infos.get(&height)?.mining_tx_index;
let tx_blob = table_tx_blobs.get(&miner_tx_id)?;
Transaction::read(&mut tx_blob.0.as_slice())?.hash()
}
};

Ok(OutputOnChain {
height: output.height as usize,
time_lock,
key,
commitment,
txid,
})
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function now returns the txid for each output due to https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#get_outs.

This is more expensive, the txid is not required by default as well. Should this be something like txid: Option<[u8; 32]> instead?

Comment on lines 508 to 543
fn key_images_spent(env: &ConcreteEnv, key_images: HashSet<KeyImage>) -> ResponseResult {
fn key_images_spent(env: &ConcreteEnv, key_images: Vec<KeyImage>) -> ResponseResult {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was changed to Vec due to https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#is_key_image_spent behavior.

curl http://127.0.0.1:18081/is_key_image_spent -d '{"key_images":["8d1bd8181bf7d857bdb281e0153d84cd55a3fcaa57c3e570f4a49f935850b5e3","8d1bd8181bf7d857bdb281e0153d84cd55a3fcaa57c3e570f4a49f935850b5e3"]}' -H 'Content-Type: application/json'
{
  "credits": 0,
  "spent_status": [1,1],
  "status": "OK",
  "top_hash": "",
  "untrusted": false
}

If cuprated used HashSet then it would respond with something like "spent_status": [1]. Small detail but I think it could matter with wallets due to indexing code depending on this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kinda want to say this should be a separate request, I can imagine it being more efficient to check if any one KI is spent rather than always needing to check every KIs spent status in the blockchain.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hinto-janai hinto-janai marked this pull request as ready for review December 25, 2024 23:51
@hinto-janai hinto-janai requested a review from Boog900 December 25, 2024 23:51
Copy link
Member

@Boog900 Boog900 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just leaving the review in the current state will probably do another pass. Looks good so far but there are defiantly areas where we could improve performance when the inner services get the required requests filled in.

Comment on lines 185 to 198
let Some((index, start_height)) =
blockchain::find_first_unknown(&mut state.blockchain_read, hashes).await?
else {
return Err(anyhow!("Failed"));
};

let m_blocks_ids = bytes.split_off(index);

Ok(GetHashesResponse {
base: helper::access_response_base(false),
m_blocks_ids,
start_height,
current_height,
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems to only be doing part of the request - finding the split point, we also need to give the next hashes in the chain back. This can be all done with BlockchainReadRequest::NextChainEntry

Comment on lines +43 to +51
// FIXME: is there a cheaper way to get this?
let difficulty = blockchain_context::batch_get_difficulties(
&mut state.blockchain_context,
vec![(height, hardfork)],
)
.await?
.first()
.copied()
.ok_or_else(|| anyhow!("Failed to get block difficulty"))?;
Copy link
Member

@Boog900 Boog900 Jan 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// FIXME: is there a cheaper way to get this?

yes - get the cumulative difficulty of this block take away the cumulative difficulty of the block before it will give you the difficulty of this block. Cumulative difficulty should be in the extended header. Also this current method is incorrect as this request takes in a timestamp not height.

Separate but related: I think we should create a specific DB request for as much data as we can as we currently need to call the service multiple times for this request.

This can all be a TODO, no need to do in this PR.

Comment on lines 149 to 159

// TODO: this is hardcoded for the current address scheme + mainnet,
// create/use a more well-defined wallet lib.
let parse_wallet_address = || {
if request.wallet_address.len() == 95 {
Ok(())
} else {
Err(())
}
};
let is_correct_address_type = || !request.wallet_address.starts_with("4");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mut state: CupratedRpcHandler,
request: GetBlockHeaderByHeightRequest,
) -> Result<GetBlockHeaderByHeightResponse, Error> {
helper::check_height(&mut state, request.height).await?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a TODO, this check should be done in the DB to prevent needing to do multiple DB requests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(there are lots of places where we could improve performance, could just be left as a wider todo)

Comment on lines 476 to 477
let block_weight_limit = usize_to_u64(c.effective_median_weight);
let block_weight_median = usize_to_u64(c.median_weight_for_block_reward);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let block_weight_limit = usize_to_u64(c.effective_median_weight);
let block_weight_median = usize_to_u64(c.median_weight_for_block_reward);
let block_weight_limit = usize_to_u64(c.effective_median_weight * 2);
let block_weight_median = usize_to_u64(c.effective_median_weight);

that matches monerod behavior but technically it should be this:

Suggested change
let block_weight_limit = usize_to_u64(c.effective_median_weight);
let block_weight_median = usize_to_u64(c.median_weight_for_block_reward);
let block_weight_limit = usize_to_u64(c.median_weight_for_block_reward * 2);
let block_weight_median = usize_to_u64(c.median_weight_for_block_reward);

Both will be the same for the current HF although past HFs these values are different. Should we copy monerod's bad behavior here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy for now, submit patch to monerod eventually, then update cuprated?

let major_version = c.current_hf.as_u8();
let height = usize_to_u64(c.chain_height);
let prev_id = Hex(c.top_hash);
let seed_hash = Hex(c.top_hash);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the Rx seed hash, it'll have to be a todo, I'll add it to the context in a future PR.

let prev_id = Hex(c.top_hash);
let seed_hash = Hex(c.top_hash);
let difficulty = c.next_difficulty.hex_prefix();
let median_weight = usize_to_u64(c.median_weight_for_block_reward);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the other comment, this is actually the correct value but if we are trying to match Monero, the value should be effective_median_weight.

tokio::time::sleep(Duration::from_millis(100)).await;
}

tokio::task::spawn_blocking(|| add_aux_pow_inner(state, request)).await?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should spawn on a rayon thread, using rayon_spawn_async

Comment on lines 308 to 328
blockchain::key_images_spent(&mut state.blockchain_read, key_images.clone())
.await?
.into_iter()
.for_each(|ki| {
if ki {
spent_status.push(KeyImageSpentStatus::SpentInBlockchain.to_u8());
} else {
spent_status.push(KeyImageSpentStatus::Unspent.to_u8());
}
});

txpool::key_images_spent(&mut state.txpool_read, key_images, !restricted)
.await?
.into_iter()
.for_each(|ki| {
if ki {
spent_status.push(KeyImageSpentStatus::SpentInPool.to_u8());
} else {
spent_status.push(KeyImageSpentStatus::Unspent.to_u8());
}
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be returning a list with double the length of the key images? I don't think this is correct. We should have a list the exact same length right

index_map.into_values().map(|out| OutKeyBin {
key: out.key.map_or([0; 32], |e| e.compress().0),
mask: out.commitment.compress().0,
unlocked: matches!(out.time_lock, Timelock::None),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should return true even if the output is locked but the lock time has passed right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

types/types/src/blockchain.rs Show resolved Hide resolved
types/types/src/blockchain.rs Show resolved Hide resolved
Comment on lines +584 to +605
/// [`BlockchainReadRequest::KeyImagesSpentVec`].
fn key_images_spent_vec(env: &ConcreteEnv, key_images: Vec<KeyImage>) -> ResponseResult {
// Prepare tx/tables in `ThreadLocal`.
let env_inner = env.env_inner();
let tx_ro = thread_local(env);
let tables = thread_local(env);

// Key image check function.
let key_image_exists = |key_image| {
let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?;
let tables = get_tables!(env_inner, tx_ro, tables)?.as_ref();
key_image_exists(&key_image, tables.key_images())
};

// Collect results using `rayon`.
Ok(BlockchainResponse::KeyImagesSpentVec(
key_images
.into_par_iter()
.map(key_image_exists)
.collect::<DbResult<_>>()?,
))
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3/3

DB operation, same as before just added _vec.

Comment on lines +51 to +63
for (_, index_vec) in outputs {
for (_, out) in index_vec {
let out_key = OutKeyBin {
key: out.key.map_or([0; 32], |e| e.compress().0),
mask: out.commitment.compress().0,
unlocked: helper::timelock_is_unlocked(&mut state, out.time_lock).await?,
height: usize_to_u64(out.height),
txid: if request.get_txid { out.txid } else { [0; 32] },
};

outs.push(out_key);
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1/2

Fixed timelock check, although now it is for due to async.

TODO: optimized async iterators?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Outputs unlock time is not just done by looking at the timestamp/block height, it has specific rules, here is the function in Cuprate:

/// Checks if an outputs unlock time has passed.
///
/// <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html>
pub const fn output_unlocked(

You will need to get the blockchian context for the time for unlock & chain height:

/// Returns the timestamp the should be used when checking locked outputs.
///
/// ref: <https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#getting-the-current-time>
pub fn current_adjusted_timestamp_for_time_lock(&self) -> u64 {

That only has to be done once, you could reuse the context for each output

Comment on lines +174 to +189
/// Returns `true` if the `timelock` is unlocked.
pub(super) async fn timelock_is_unlocked(
state: &mut CupratedRpcHandler,
timelock: Timelock,
) -> Result<bool, Error> {
let unlocked = match timelock {
Timelock::None => true,
Timelock::Block(height) => {
let (top_height, _) = top_height(state).await?;
top_height > usize_to_u64(height)
}
Timelock::Time(timestamp) => cuprate_helper::time::current_unix_timestamp() > timestamp,
};

Ok(unlocked)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2/2

Timelock check function.

Maybe TODO: should this be a BlockchainReadRequest? I feel like this could be used elsewhere.

@hinto-janai hinto-janai requested a review from Boog900 January 17, 2025 21:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-binaries Related to binaries. A-book-architecture Related to the Architecture book. A-books Related to Cuprate's books. A-consensus Related to consensus. A-dependency Related to dependencies, or changes to a Cargo.{toml,lock} file. A-docs Related to documentation. A-helper Related to cuprate-helper. A-net Related to networking. A-p2p Related to P2P. A-rpc Related to RPC. A-storage Related to storage. A-types Related to types. A-workspace Changes to a root workspace file or general repo file. A-zmq Related to ZMQ.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants