-
Notifications
You must be signed in to change notification settings - Fork 26
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: Blockchain Manager #274
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A lot of this code still needs a bit of work, however I am starting to need to build other sections so to prevent a massive PR for the whole binary this will have to do.
//! The blockchain manger interface. | ||
//! | ||
//! This module contains all the functions to mutate the blockchain's state in any way, through the | ||
//! blockchain manger. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file contains the interface for the blockchain manager.
I decided to not go with a tower::service interface, as I just don't think it is needed here. The benefits of a Service interface here would just be uniformity with every other service, the drawbacks are the extra over head of holding/cloning another handle (admittedly low) and just the extra hassle of holding another service handle.
I think these free functions as a way to mutate the blockchain makes more sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't you say this about every Service
though? Breaking the pattern and using global static
s seems lazy? This is yet another interface that must be learned and it doesn't come with docs on how to use it.
If it's a 1-off in cuprated
I think it's fine but there should be documentation on what the statics are, how to use generally use the interface, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fair, the API here should be pretty easy to use - all you need to do is call the functions, I'll add some more docs.
Couldn't you say this about every Service though
Other service are exposed this one is not. There are other benefits to the Service interface, like the wide range of middleware but since we are not going to make use of them for the blockchain manager that isn't a benefit here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only nits, left another review.
blockchain_write_handle | ||
.ready() | ||
.await | ||
.expect(PANIC_CRITICAL_SERVICE_ERROR) | ||
.call(BlockchainWriteRequest::WriteBlock( | ||
VerifiedBlockInformation { | ||
block_blob: genesis.serialize(), | ||
txs: vec![], | ||
block_hash: genesis.hash(), | ||
pow_hash: cryptonight_hash_v0(&genesis.serialize_pow_hash()), | ||
height: 0, | ||
generated_coins: genesis.miner_transaction.prefix().outputs[0] | ||
.amount | ||
.unwrap(), | ||
weight: genesis.miner_transaction.weight(), | ||
long_term_weight: genesis.miner_transaction.weight(), | ||
cumulative_difficulty: 1, | ||
block: genesis, | ||
}, | ||
)) | ||
.await | ||
.expect(PANIC_CRITICAL_SERVICE_ERROR); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't need to change for now but I still think cuprate_constants::genesis
should exist - having the genesis block inlined in some async function in cuprated
feels wrong. It's not about de-duplicatation but more about separating concerns and being clean, these constants deserve a name instead of being inlined.
Also, this is only mainnet right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
having the genesis block inlined in some async function in cuprated feels wrong
This is just mapping the genesis block got from generate_genesis_block
to VerifiedBlockInformation
.
Also, this is only mainnet right?
No this should work on all 3 current networks, although it does (like monerod) make some assumptions about the genesis blocks format
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mapping
I realize this, I'm saying this mapping should exist in cuprate_constants::genesis
as well, next to the Block
.
//! The blockchain manger interface. | ||
//! | ||
//! This module contains all the functions to mutate the blockchain's state in any way, through the | ||
//! blockchain manger. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't you say this about every Service
though? Breaking the pattern and using global static
s seems lazy? This is yet another interface that must be learned and it doesn't come with docs on how to use it.
If it's a 1-off in cuprated
I think it's fine but there should be documentation on what the statics are, how to use generally use the interface, etc.
}; | ||
|
||
/// The channel used to send [`BlockchainManagerCommand`]s to the blockchain manger. | ||
pub static COMMAND_TX: OnceLock<mpsc::Sender<BlockchainManagerCommand>> = OnceLock::new(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having a pub static OnceLock
read/write available to the entire binary doesn't seem necessary, there should also be docs on who/when/where this OnceLock
should be initialized.
Can this be privately defined where it is initiated then accompanied with:
/// Access to the blockchain manager's [...]
///
/// # Invariant
/// Must be initialized with [...]
///
/// # Panics
/// Panics if not initialized in [...]
pub fn command_tx() -> &mpsc::Sender<BlockchainManagerCommand> {
&*COMMAND_TX.get().expect("wasn't initialized")
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops yeah wasn't meant to be fully public
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added some more docs to this
/// This is used over something like a dashmap as we expect a lot of collisions in a short amount of | ||
/// time for new blocks so we would lose the benefit of sharded locks. A dashmap is made up of `RwLocks` | ||
/// which are also more expensive than `Mutex`s. | ||
pub static BLOCKS_BEING_HANDLED: OnceLock<Mutex<HashSet<[u8; 32]>>> = OnceLock::new(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need to be pub
? It's only currently used in this file.
To be honest pub static Mutex
is pretty bad code smell - global resources like this really should be documented with who/when/how it's used.
If this only needs to be mutated within this file, what about doing the same as COMMAND_TX
? Make this private and expose a fn() -> &HashSet<_, _>
while documented that it takes a lock, and the usage patterns of that lock.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've moved it into the only function it is needed in + made it a LazyLock
// TODO: remove this when we have a working tx-pool. | ||
if given_txs.len() != block.transactions.len() { | ||
return Err(IncomingBlockError::UnknownTransactions( | ||
block_hash, | ||
(0..usize_to_u64(block.transactions.len())).collect(), | ||
)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why usize_to_u64
? Shouldn't it be usize
if it's an index?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These values are sent over the P2P network, which requires a u64
. I could change this and probably will when the tx-pool is made and do the conversion in the P2P hanlder.
However I would rather wait to make the tx-pool rather than preemptively change this.
+ 'static, | ||
CN::Future: Send + 'static, | ||
{ | ||
tracing::info!("Starting blockchain syncer"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe use tracing::{info,debug,trace}
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is fine - this file will be completely changed at some point as well
|
||
impl Service<ChainSvcRequest> for ChainService { | ||
type Response = ChainSvcResponse; | ||
type Error = tower::BoxError; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
anyhow::Error
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It needs to match the bounds in the p2p crate, I am planning to change those in a separate PR though
/// | ||
/// A [`RwLock`] where a write lock is taken during a reorg and a read lock can be taken | ||
/// for any operation which must complete without a reorg happening. | ||
pub static REORG_LOCK: RwLock<()> = RwLock::const_new(()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have mixed feelings using locks in a C-style like this... The solution is to design things such that you can scope and pass resources properly, I'm not going to ask this to be changed since that will involve a lot of work but I hope you realize this pattern is bad as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
related issue: #305
pub static BLOCKS_BEING_HANDLED: OnceLock<Mutex<HashSet<[u8; 32]>>> = OnceLock::new(); | ||
/// This channel is initialized in [`init_blockchain_manager`](super::manager::init_blockchain_manager), the functions | ||
/// in this file document what happens if this is not initialized when they are called. | ||
pub(super) static COMMAND_TX: OnceLock<mpsc::Sender<BlockchainManagerCommand>> = OnceLock::new(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This (super)
is fine but I would have preferred if it were moved to manager.rs
and kept private - it is still writeable from interface.rs
since it's defined there, even though it only needs a &mpsc::Sender
.
Doesn't matter too much I guess but then again things being scoped is nice.
static BLOCKS_BEING_HANDLED: LazyLock<Mutex<HashSet<[u8; 32]>>> = | ||
LazyLock::new(|| Mutex::new(HashSet::new())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could/should this be a tokio::sync::Mutex
?
As it is now, if handle_incoming_block
is asynchronously called from many many places, the std::sync::Mutex
may block the executor more than tokio::sync::Mutex
, right?
Although IDK really, the lock isn't held for long so this probably doesn't matter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although std Mutex is blocking we hold the lock for a tiny amount of time + an async Mutex is a lot more expensive: https://docs.rs/tokio/latest/tokio/sync/struct.Mutex.html#which-kind-of-mutex-should-you-use
/// Broadcast a valid block to the network. | ||
async fn broadcast_block(&mut self, block_bytes: Bytes, blockchain_height: usize) { | ||
self.broadcast_svc | ||
.ready() | ||
.await | ||
.expect("Broadcast service cannot error.") | ||
.call(BroadcastRequest::Block { | ||
block_bytes, | ||
current_blockchain_height: usize_to_u64(blockchain_height), | ||
}) | ||
.await | ||
.expect("Broadcast service cannot error."); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I see. Maybe Broadcast service is Infallible
would work then, current msg is fine too.
What
This PR contains the blockchain manager, there are still quite a few TODOs however I think it is in a state to be reviewed and merged.
More details in comments