Merge branch 'main' into lints-6

This commit is contained in:
hinto.janai 2024-09-19 17:31:22 -04:00
commit b9dd6256e2
No known key found for this signature in database
GPG key ID: D47CE05FA175A499
50 changed files with 2281 additions and 1046 deletions

842
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -9,8 +9,8 @@ repository = "https://github.com/Cuprate/cuprate/tree/main/consensus"
[features]
# All features on by default.
default = ["std", "atomic", "asynch", "cast", "fs", "num", "map", "time", "thread", "constants"]
# All features off by default.
default = []
std = []
atomic = ["dep:crossbeam"]
asynch = ["dep:futures", "dep:rayon"]
@ -21,6 +21,7 @@ num = []
map = ["cast", "dep:monero-serai"]
time = ["dep:chrono", "std"]
thread = ["std", "dep:target_os_lib"]
tx = ["dep:monero-serai"]
[dependencies]
crossbeam = { workspace = true, optional = true }
@ -39,7 +40,8 @@ target_os_lib = { package = "windows", version = ">=0.51", features = ["Win32_Sy
target_os_lib = { package = "libc", version = "0.2.151", optional = true }
[dev-dependencies]
tokio = { workspace = true, features = ["full"] }
tokio = { workspace = true, features = ["full"] }
curve25519-dalek = { workspace = true }
[lints]
workspace = true

View file

@ -31,6 +31,8 @@ pub mod thread;
#[cfg(feature = "time")]
pub mod time;
#[cfg(feature = "tx")]
pub mod tx;
//---------------------------------------------------------------------------------------------------- Private Usage
//----------------------------------------------------------------------------------------------------

70
helper/src/tx.rs Normal file
View file

@ -0,0 +1,70 @@
//! Utils for working with [`Transaction`]
use monero_serai::transaction::{Input, Transaction};
/// Calculates the fee of the [`Transaction`].
///
/// # Panics
/// This will panic if the inputs overflow or the transaction outputs too much, so should only
/// be used on known to be valid txs.
pub fn tx_fee(tx: &Transaction) -> u64 {
let mut fee = 0_u64;
match &tx {
Transaction::V1 { prefix, .. } => {
for input in &prefix.inputs {
match input {
Input::Gen(_) => return 0,
Input::ToKey { amount, .. } => {
fee = fee.checked_add(amount.unwrap_or(0)).unwrap();
}
}
}
for output in &prefix.outputs {
fee = fee.checked_sub(output.amount.unwrap_or(0)).unwrap();
}
}
Transaction::V2 { proofs, .. } => {
fee = proofs.as_ref().unwrap().base.fee;
}
};
fee
}
#[cfg(test)]
mod test {
use curve25519_dalek::{edwards::CompressedEdwardsY, EdwardsPoint};
use monero_serai::transaction::{NotPruned, Output, Timelock, TransactionPrefix};
use super::*;
#[test]
#[should_panic(expected = "called `Option::unwrap()` on a `None` value")]
fn tx_fee_panic() {
let input = Input::ToKey {
amount: Some(u64::MAX),
key_offsets: vec![],
key_image: EdwardsPoint::default(),
};
let output = Output {
amount: Some(u64::MAX),
key: CompressedEdwardsY::default(),
view_tag: None,
};
let tx = Transaction::<NotPruned>::V1 {
prefix: TransactionPrefix {
additional_timelock: Timelock::None,
inputs: vec![input; 2],
outputs: vec![output],
extra: vec![],
},
signatures: vec![],
};
tx_fee(&tx);
}
}

View file

@ -17,3 +17,6 @@ serde = { workspace = true, features = ["derive"], optional = true }
[dev-dependencies]
serde_json = { workspace = true, features = ["std"] }
[lints]
workspace = true

View file

@ -22,17 +22,15 @@ pub enum FixedByteError {
}
impl FixedByteError {
fn field_name(&self) -> &'static str {
const fn field_name(&self) -> &'static str {
match self {
FixedByteError::InvalidLength => "input",
Self::InvalidLength => "input",
}
}
fn field_data(&self) -> &'static str {
const fn field_data(&self) -> &'static str {
match self {
FixedByteError::InvalidLength => {
"Cannot create fix byte array, input has invalid length."
}
Self::InvalidLength => "Cannot create fix byte array, input has invalid length.",
}
}
}
@ -82,7 +80,7 @@ impl<const N: usize> ByteArray<N> {
impl<const N: usize> From<[u8; N]> for ByteArray<N> {
fn from(value: [u8; N]) -> Self {
ByteArray(Bytes::copy_from_slice(&value))
Self(Bytes::copy_from_slice(&value))
}
}
@ -101,7 +99,7 @@ impl<const N: usize> TryFrom<Bytes> for ByteArray<N> {
if value.len() != N {
return Err(FixedByteError::InvalidLength);
}
Ok(ByteArray(value))
Ok(Self(value))
}
}
@ -112,7 +110,7 @@ impl<const N: usize> TryFrom<Vec<u8>> for ByteArray<N> {
if value.len() != N {
return Err(FixedByteError::InvalidLength);
}
Ok(ByteArray(Bytes::from(value)))
Ok(Self(Bytes::from(value)))
}
}
@ -142,11 +140,11 @@ impl<'de, const N: usize> Deserialize<'de> for ByteArrayVec<N> {
}
impl<const N: usize> ByteArrayVec<N> {
pub fn len(&self) -> usize {
pub const fn len(&self) -> usize {
self.0.len() / N
}
pub fn is_empty(&self) -> bool {
pub const fn is_empty(&self) -> bool {
self.len() == 0
}
@ -162,6 +160,7 @@ impl<const N: usize> ByteArrayVec<N> {
///
/// # Panics
/// Panics if at > len.
#[must_use]
pub fn split_off(&mut self, at: usize) -> Self {
Self(self.0.split_off(at * N))
}
@ -169,9 +168,9 @@ impl<const N: usize> ByteArrayVec<N> {
impl<const N: usize> From<&ByteArrayVec<N>> for Vec<[u8; N]> {
fn from(value: &ByteArrayVec<N>) -> Self {
let mut out = Vec::with_capacity(value.len());
let mut out = Self::with_capacity(value.len());
for i in 0..value.len() {
out.push(value[i])
out.push(value[i]);
}
out
@ -181,11 +180,11 @@ impl<const N: usize> From<&ByteArrayVec<N>> for Vec<[u8; N]> {
impl<const N: usize> From<Vec<[u8; N]>> for ByteArrayVec<N> {
fn from(value: Vec<[u8; N]>) -> Self {
let mut bytes = BytesMut::with_capacity(N * value.len());
for i in value.into_iter() {
bytes.extend_from_slice(&i)
for i in value {
bytes.extend_from_slice(&i);
}
ByteArrayVec(bytes.freeze())
Self(bytes.freeze())
}
}
@ -197,13 +196,13 @@ impl<const N: usize> TryFrom<Bytes> for ByteArrayVec<N> {
return Err(FixedByteError::InvalidLength);
}
Ok(ByteArrayVec(value))
Ok(Self(value))
}
}
impl<const N: usize> From<[u8; N]> for ByteArrayVec<N> {
fn from(value: [u8; N]) -> Self {
ByteArrayVec(Bytes::copy_from_slice(value.as_slice()))
Self(Bytes::copy_from_slice(value.as_slice()))
}
}
@ -211,11 +210,11 @@ impl<const N: usize, const LEN: usize> From<[[u8; N]; LEN]> for ByteArrayVec<N>
fn from(value: [[u8; N]; LEN]) -> Self {
let mut bytes = BytesMut::with_capacity(N * LEN);
for val in value.into_iter() {
for val in value {
bytes.put_slice(val.as_slice());
}
ByteArrayVec(bytes.freeze())
Self(bytes.freeze())
}
}
@ -227,7 +226,7 @@ impl<const N: usize> TryFrom<Vec<u8>> for ByteArrayVec<N> {
return Err(FixedByteError::InvalidLength);
}
Ok(ByteArrayVec(Bytes::from(value)))
Ok(Self(Bytes::from(value)))
}
}
@ -235,9 +234,12 @@ impl<const N: usize> Index<usize> for ByteArrayVec<N> {
type Output = [u8; N];
fn index(&self, index: usize) -> &Self::Output {
if (index + 1) * N > self.0.len() {
panic!("Index out of range, idx: {}, length: {}", index, self.len());
}
assert!(
(index + 1) * N <= self.0.len(),
"Index out of range, idx: {}, length: {}",
index,
self.len()
);
self.0[index * N..(index + 1) * N]
.as_ref()

View file

@ -15,7 +15,7 @@ cuprate-levin = { path = "../levin" }
cuprate-epee-encoding = { path = "../epee-encoding" }
cuprate-fixed-bytes = { path = "../fixed-bytes" }
cuprate-types = { path = "../../types", default-features = false, features = ["epee"] }
cuprate-helper = { path = "../../helper", default-features = false, features = ["cast"] }
cuprate-helper = { path = "../../helper", default-features = false, features = ["map"] }
bitflags = { workspace = true, features = ["std"] }
bytes = { workspace = true, features = ["std"] }
@ -24,3 +24,5 @@ thiserror = { workspace = true }
[dev-dependencies]
hex = { workspace = true, features = ["std"]}
[lints]
workspace = true

View file

@ -51,38 +51,38 @@ impl EpeeObject for NetworkAddress {
}
impl NetworkAddress {
pub fn get_zone(&self) -> NetZone {
pub const fn get_zone(&self) -> NetZone {
match self {
NetworkAddress::Clear(_) => NetZone::Public,
Self::Clear(_) => NetZone::Public,
}
}
pub fn is_loopback(&self) -> bool {
pub const fn is_loopback(&self) -> bool {
// TODO
false
}
pub fn is_local(&self) -> bool {
pub const fn is_local(&self) -> bool {
// TODO
false
}
pub fn port(&self) -> u16 {
pub const fn port(&self) -> u16 {
match self {
NetworkAddress::Clear(ip) => ip.port(),
Self::Clear(ip) => ip.port(),
}
}
}
impl From<net::SocketAddrV4> for NetworkAddress {
fn from(value: net::SocketAddrV4) -> Self {
NetworkAddress::Clear(value.into())
Self::Clear(value.into())
}
}
impl From<net::SocketAddrV6> for NetworkAddress {
fn from(value: net::SocketAddrV6) -> Self {
NetworkAddress::Clear(value.into())
Self::Clear(value.into())
}
}

View file

@ -74,7 +74,7 @@ impl From<NetworkAddress> for TaggedNetworkAddress {
fn from(value: NetworkAddress) -> Self {
match value {
NetworkAddress::Clear(addr) => match addr {
SocketAddr::V4(addr) => TaggedNetworkAddress {
SocketAddr::V4(addr) => Self {
ty: Some(1),
addr: Some(AllFieldsNetworkAddress {
m_ip: Some(u32::from_be_bytes(addr.ip().octets())),
@ -82,7 +82,7 @@ impl From<NetworkAddress> for TaggedNetworkAddress {
addr: None,
}),
},
SocketAddr::V6(addr) => TaggedNetworkAddress {
SocketAddr::V6(addr) => Self {
ty: Some(2),
addr: Some(AllFieldsNetworkAddress {
addr: Some(addr.ip().octets()),

View file

@ -55,27 +55,27 @@ pub enum LevinCommand {
impl std::fmt::Display for LevinCommand {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let LevinCommand::Unknown(id) = self {
return f.write_str(&format!("unknown id: {}", id));
if let Self::Unknown(id) = self {
return f.write_str(&format!("unknown id: {id}"));
}
f.write_str(match self {
LevinCommand::Handshake => "handshake",
LevinCommand::TimedSync => "timed sync",
LevinCommand::Ping => "ping",
LevinCommand::SupportFlags => "support flags",
Self::Handshake => "handshake",
Self::TimedSync => "timed sync",
Self::Ping => "ping",
Self::SupportFlags => "support flags",
LevinCommand::NewBlock => "new block",
LevinCommand::NewTransactions => "new transactions",
LevinCommand::GetObjectsRequest => "get objects request",
LevinCommand::GetObjectsResponse => "get objects response",
LevinCommand::ChainRequest => "chain request",
LevinCommand::ChainResponse => "chain response",
LevinCommand::NewFluffyBlock => "new fluffy block",
LevinCommand::FluffyMissingTxsRequest => "fluffy missing transaction request",
LevinCommand::GetTxPoolCompliment => "get transaction pool compliment",
Self::NewBlock => "new block",
Self::NewTransactions => "new transactions",
Self::GetObjectsRequest => "get objects request",
Self::GetObjectsResponse => "get objects response",
Self::ChainRequest => "chain request",
Self::ChainResponse => "chain response",
Self::NewFluffyBlock => "new fluffy block",
Self::FluffyMissingTxsRequest => "fluffy missing transaction request",
Self::GetTxPoolCompliment => "get transaction pool compliment",
LevinCommand::Unknown(_) => unreachable!(),
Self::Unknown(_) => unreachable!(),
})
}
}
@ -83,50 +83,51 @@ impl std::fmt::Display for LevinCommand {
impl LevinCommandTrait for LevinCommand {
fn bucket_size_limit(&self) -> u64 {
// https://github.com/monero-project/monero/blob/00fd416a99686f0956361d1cd0337fe56e58d4a7/src/cryptonote_basic/connection_context.cpp#L37
#[expect(clippy::match_same_arms, reason = "formatting is more clear")]
match self {
LevinCommand::Handshake => 65536,
LevinCommand::TimedSync => 65536,
LevinCommand::Ping => 4096,
LevinCommand::SupportFlags => 4096,
Self::Handshake => 65536,
Self::TimedSync => 65536,
Self::Ping => 4096,
Self::SupportFlags => 4096,
LevinCommand::NewBlock => 1024 * 1024 * 128, // 128 MB (max packet is a bit less than 100 MB though)
LevinCommand::NewTransactions => 1024 * 1024 * 128, // 128 MB (max packet is a bit less than 100 MB though)
LevinCommand::GetObjectsRequest => 1024 * 1024 * 2, // 2 MB
LevinCommand::GetObjectsResponse => 1024 * 1024 * 128, // 128 MB (max packet is a bit less than 100 MB though)
LevinCommand::ChainRequest => 512 * 1024, // 512 kB
LevinCommand::ChainResponse => 1024 * 1024 * 4, // 4 MB
LevinCommand::NewFluffyBlock => 1024 * 1024 * 4, // 4 MB
LevinCommand::FluffyMissingTxsRequest => 1024 * 1024, // 1 MB
LevinCommand::GetTxPoolCompliment => 1024 * 1024 * 4, // 4 MB
Self::NewBlock => 1024 * 1024 * 128, // 128 MB (max packet is a bit less than 100 MB though)
Self::NewTransactions => 1024 * 1024 * 128, // 128 MB (max packet is a bit less than 100 MB though)
Self::GetObjectsRequest => 1024 * 1024 * 2, // 2 MB
Self::GetObjectsResponse => 1024 * 1024 * 128, // 128 MB (max packet is a bit less than 100 MB though)
Self::ChainRequest => 512 * 1024, // 512 kB
Self::ChainResponse => 1024 * 1024 * 4, // 4 MB
Self::NewFluffyBlock => 1024 * 1024 * 4, // 4 MB
Self::FluffyMissingTxsRequest => 1024 * 1024, // 1 MB
Self::GetTxPoolCompliment => 1024 * 1024 * 4, // 4 MB
LevinCommand::Unknown(_) => u64::MAX,
Self::Unknown(_) => u64::MAX,
}
}
fn is_handshake(&self) -> bool {
matches!(self, LevinCommand::Handshake)
matches!(self, Self::Handshake)
}
}
impl From<u32> for LevinCommand {
fn from(value: u32) -> Self {
match value {
1001 => LevinCommand::Handshake,
1002 => LevinCommand::TimedSync,
1003 => LevinCommand::Ping,
1007 => LevinCommand::SupportFlags,
1001 => Self::Handshake,
1002 => Self::TimedSync,
1003 => Self::Ping,
1007 => Self::SupportFlags,
2001 => LevinCommand::NewBlock,
2002 => LevinCommand::NewTransactions,
2003 => LevinCommand::GetObjectsRequest,
2004 => LevinCommand::GetObjectsResponse,
2006 => LevinCommand::ChainRequest,
2007 => LevinCommand::ChainResponse,
2008 => LevinCommand::NewFluffyBlock,
2009 => LevinCommand::FluffyMissingTxsRequest,
2010 => LevinCommand::GetTxPoolCompliment,
2001 => Self::NewBlock,
2002 => Self::NewTransactions,
2003 => Self::GetObjectsRequest,
2004 => Self::GetObjectsResponse,
2006 => Self::ChainRequest,
2007 => Self::ChainResponse,
2008 => Self::NewFluffyBlock,
2009 => Self::FluffyMissingTxsRequest,
2010 => Self::GetTxPoolCompliment,
x => LevinCommand::Unknown(x),
x => Self::Unknown(x),
}
}
}
@ -191,19 +192,19 @@ pub enum ProtocolMessage {
}
impl ProtocolMessage {
pub fn command(&self) -> LevinCommand {
pub const fn command(&self) -> LevinCommand {
use LevinCommand as C;
match self {
ProtocolMessage::NewBlock(_) => C::NewBlock,
ProtocolMessage::NewFluffyBlock(_) => C::NewFluffyBlock,
ProtocolMessage::GetObjectsRequest(_) => C::GetObjectsRequest,
ProtocolMessage::GetObjectsResponse(_) => C::GetObjectsResponse,
ProtocolMessage::ChainRequest(_) => C::ChainRequest,
ProtocolMessage::ChainEntryResponse(_) => C::ChainResponse,
ProtocolMessage::NewTransactions(_) => C::NewTransactions,
ProtocolMessage::FluffyMissingTransactionsRequest(_) => C::FluffyMissingTxsRequest,
ProtocolMessage::GetTxPoolCompliment(_) => C::GetTxPoolCompliment,
Self::NewBlock(_) => C::NewBlock,
Self::NewFluffyBlock(_) => C::NewFluffyBlock,
Self::GetObjectsRequest(_) => C::GetObjectsRequest,
Self::GetObjectsResponse(_) => C::GetObjectsResponse,
Self::ChainRequest(_) => C::ChainRequest,
Self::ChainEntryResponse(_) => C::ChainResponse,
Self::NewTransactions(_) => C::NewTransactions,
Self::FluffyMissingTransactionsRequest(_) => C::FluffyMissingTxsRequest,
Self::GetTxPoolCompliment(_) => C::GetTxPoolCompliment,
}
}
@ -230,26 +231,26 @@ impl ProtocolMessage {
use LevinCommand as C;
match self {
ProtocolMessage::NewBlock(val) => build_message(C::NewBlock, val, builder)?,
ProtocolMessage::NewTransactions(val) => {
build_message(C::NewTransactions, val, builder)?
Self::NewBlock(val) => build_message(C::NewBlock, val, builder)?,
Self::NewTransactions(val) => {
build_message(C::NewTransactions, val, builder)?;
}
ProtocolMessage::GetObjectsRequest(val) => {
build_message(C::GetObjectsRequest, val, builder)?
Self::GetObjectsRequest(val) => {
build_message(C::GetObjectsRequest, val, builder)?;
}
ProtocolMessage::GetObjectsResponse(val) => {
build_message(C::GetObjectsResponse, val, builder)?
Self::GetObjectsResponse(val) => {
build_message(C::GetObjectsResponse, val, builder)?;
}
ProtocolMessage::ChainRequest(val) => build_message(C::ChainRequest, val, builder)?,
ProtocolMessage::ChainEntryResponse(val) => {
build_message(C::ChainResponse, val, builder)?
Self::ChainRequest(val) => build_message(C::ChainRequest, val, builder)?,
Self::ChainEntryResponse(val) => {
build_message(C::ChainResponse, val, builder)?;
}
ProtocolMessage::NewFluffyBlock(val) => build_message(C::NewFluffyBlock, val, builder)?,
ProtocolMessage::FluffyMissingTransactionsRequest(val) => {
build_message(C::FluffyMissingTxsRequest, val, builder)?
Self::NewFluffyBlock(val) => build_message(C::NewFluffyBlock, val, builder)?,
Self::FluffyMissingTransactionsRequest(val) => {
build_message(C::FluffyMissingTxsRequest, val, builder)?;
}
ProtocolMessage::GetTxPoolCompliment(val) => {
build_message(C::GetTxPoolCompliment, val, builder)?
Self::GetTxPoolCompliment(val) => {
build_message(C::GetTxPoolCompliment, val, builder)?;
}
}
Ok(())
@ -265,14 +266,14 @@ pub enum AdminRequestMessage {
}
impl AdminRequestMessage {
pub fn command(&self) -> LevinCommand {
pub const fn command(&self) -> LevinCommand {
use LevinCommand as C;
match self {
AdminRequestMessage::Handshake(_) => C::Handshake,
AdminRequestMessage::Ping => C::Ping,
AdminRequestMessage::SupportFlags => C::SupportFlags,
AdminRequestMessage::TimedSync(_) => C::TimedSync,
Self::Handshake(_) => C::Handshake,
Self::Ping => C::Ping,
Self::SupportFlags => C::SupportFlags,
Self::TimedSync(_) => C::TimedSync,
}
}
@ -286,13 +287,13 @@ impl AdminRequestMessage {
cuprate_epee_encoding::from_bytes::<EmptyMessage, _>(buf)
.map_err(|e| BucketError::BodyDecodingError(e.into()))?;
AdminRequestMessage::Ping
Self::Ping
}
C::SupportFlags => {
cuprate_epee_encoding::from_bytes::<EmptyMessage, _>(buf)
.map_err(|e| BucketError::BodyDecodingError(e.into()))?;
AdminRequestMessage::SupportFlags
Self::SupportFlags
}
_ => return Err(BucketError::UnknownCommand),
})
@ -302,11 +303,11 @@ impl AdminRequestMessage {
use LevinCommand as C;
match self {
AdminRequestMessage::Handshake(val) => build_message(C::Handshake, val, builder)?,
AdminRequestMessage::TimedSync(val) => build_message(C::TimedSync, val, builder)?,
AdminRequestMessage::Ping => build_message(C::Ping, EmptyMessage, builder)?,
AdminRequestMessage::SupportFlags => {
build_message(C::SupportFlags, EmptyMessage, builder)?
Self::Handshake(val) => build_message(C::Handshake, val, builder)?,
Self::TimedSync(val) => build_message(C::TimedSync, val, builder)?,
Self::Ping => build_message(C::Ping, EmptyMessage, builder)?,
Self::SupportFlags => {
build_message(C::SupportFlags, EmptyMessage, builder)?;
}
}
Ok(())
@ -322,14 +323,14 @@ pub enum AdminResponseMessage {
}
impl AdminResponseMessage {
pub fn command(&self) -> LevinCommand {
pub const fn command(&self) -> LevinCommand {
use LevinCommand as C;
match self {
AdminResponseMessage::Handshake(_) => C::Handshake,
AdminResponseMessage::Ping(_) => C::Ping,
AdminResponseMessage::SupportFlags(_) => C::SupportFlags,
AdminResponseMessage::TimedSync(_) => C::TimedSync,
Self::Handshake(_) => C::Handshake,
Self::Ping(_) => C::Ping,
Self::SupportFlags(_) => C::SupportFlags,
Self::TimedSync(_) => C::TimedSync,
}
}
@ -349,11 +350,11 @@ impl AdminResponseMessage {
use LevinCommand as C;
match self {
AdminResponseMessage::Handshake(val) => build_message(C::Handshake, val, builder)?,
AdminResponseMessage::TimedSync(val) => build_message(C::TimedSync, val, builder)?,
AdminResponseMessage::Ping(val) => build_message(C::Ping, val, builder)?,
AdminResponseMessage::SupportFlags(val) => {
build_message(C::SupportFlags, val, builder)?
Self::Handshake(val) => build_message(C::Handshake, val, builder)?,
Self::TimedSync(val) => build_message(C::TimedSync, val, builder)?,
Self::Ping(val) => build_message(C::Ping, val, builder)?,
Self::SupportFlags(val) => {
build_message(C::SupportFlags, val, builder)?;
}
}
Ok(())
@ -368,23 +369,23 @@ pub enum Message {
}
impl Message {
pub fn is_request(&self) -> bool {
matches!(self, Message::Request(_))
pub const fn is_request(&self) -> bool {
matches!(self, Self::Request(_))
}
pub fn is_response(&self) -> bool {
matches!(self, Message::Response(_))
pub const fn is_response(&self) -> bool {
matches!(self, Self::Response(_))
}
pub fn is_protocol(&self) -> bool {
matches!(self, Message::Protocol(_))
pub const fn is_protocol(&self) -> bool {
matches!(self, Self::Protocol(_))
}
pub fn command(&self) -> LevinCommand {
pub const fn command(&self) -> LevinCommand {
match self {
Message::Request(mes) => mes.command(),
Message::Response(mes) => mes.command(),
Message::Protocol(mes) => mes.command(),
Self::Request(mes) => mes.command(),
Self::Response(mes) => mes.command(),
Self::Protocol(mes) => mes.command(),
}
}
}
@ -398,27 +399,25 @@ impl LevinBody for Message {
command: LevinCommand,
) -> Result<Self, BucketError> {
Ok(match typ {
MessageType::Request => Message::Request(AdminRequestMessage::decode(body, command)?),
MessageType::Response => {
Message::Response(AdminResponseMessage::decode(body, command)?)
}
MessageType::Notification => Message::Protocol(ProtocolMessage::decode(body, command)?),
MessageType::Request => Self::Request(AdminRequestMessage::decode(body, command)?),
MessageType::Response => Self::Response(AdminResponseMessage::decode(body, command)?),
MessageType::Notification => Self::Protocol(ProtocolMessage::decode(body, command)?),
})
}
fn encode(self, builder: &mut BucketBuilder<LevinCommand>) -> Result<(), BucketError> {
match self {
Message::Protocol(pro) => {
Self::Protocol(pro) => {
builder.set_message_type(MessageType::Notification);
builder.set_return_code(0);
pro.build(builder)
}
Message::Request(req) => {
Self::Request(req) => {
builder.set_message_type(MessageType::Request);
builder.set_return_code(0);
req.build(builder)
}
Message::Response(res) => {
Self::Response(res) => {
builder.set_message_type(MessageType::Response);
builder.set_return_code(1);
res.build(builder)

View file

@ -45,7 +45,7 @@ pub struct HandshakeResponse {
pub node_data: BasicNodeData,
/// Core Sync Data
pub payload_data: CoreSyncData,
/// PeerList
/// `PeerList`
pub local_peerlist_new: Vec<PeerListEntryBase>,
}
@ -56,7 +56,7 @@ epee_object!(
local_peerlist_new: Vec<PeerListEntryBase>,
);
/// A TimedSync Request
/// A `TimedSync` Request
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TimedSyncRequest {
/// Core Sync Data
@ -68,12 +68,12 @@ epee_object!(
payload_data: CoreSyncData,
);
/// A TimedSync Response
/// A `TimedSync` Response
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TimedSyncResponse {
/// Core Sync Data
pub payload_data: CoreSyncData,
/// PeerList
/// `PeerList`
pub local_peerlist_new: Vec<PeerListEntryBase>,
}

View file

@ -18,6 +18,7 @@
use bitflags::bitflags;
use cuprate_epee_encoding::epee_object;
use cuprate_helper::map::split_u128_into_low_high_bits;
pub use cuprate_types::{BlockCompleteEntry, PrunedTxBlobEntry, TransactionBlobs};
use crate::NetworkAddress;
@ -34,7 +35,7 @@ bitflags! {
impl From<u32> for PeerSupportFlags {
fn from(value: u32) -> Self {
PeerSupportFlags(value)
Self(value)
}
}
@ -113,16 +114,17 @@ epee_object! {
}
impl CoreSyncData {
pub fn new(
pub const fn new(
cumulative_difficulty_128: u128,
current_height: u64,
pruning_seed: u32,
top_id: [u8; 32],
top_version: u8,
) -> CoreSyncData {
let cumulative_difficulty = cumulative_difficulty_128 as u64;
let cumulative_difficulty_top64 = (cumulative_difficulty_128 >> 64) as u64;
CoreSyncData {
) -> Self {
let (cumulative_difficulty, cumulative_difficulty_top64) =
split_u128_into_low_high_bits(cumulative_difficulty_128);
Self {
cumulative_difficulty,
cumulative_difficulty_top64,
current_height,
@ -139,7 +141,7 @@ impl CoreSyncData {
}
}
/// PeerListEntryBase, information kept on a peer which will be entered
/// `PeerListEntryBase`, information kept on a peer which will be entered
/// in a peer list/store.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PeerListEntryBase {

View file

@ -127,7 +127,7 @@ pub struct ChainResponse {
impl ChainResponse {
#[inline]
pub fn cumulative_difficulty(&self) -> u128 {
pub const fn cumulative_difficulty(&self) -> u128 {
let cumulative_difficulty = self.cumulative_difficulty_top64 as u128;
cumulative_difficulty << 64 | self.cumulative_difficulty_low64 as u128
}
@ -159,7 +159,7 @@ epee_object!(
current_blockchain_height: u64,
);
/// A request for Txs we are missing from our TxPool
/// A request for Txs we are missing from our `TxPool`
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FluffyMissingTransactionsRequest {
/// The Block we are missing the Txs in
@ -177,7 +177,7 @@ epee_object!(
missing_tx_indices: Vec<u64> as ContainerAsBlob<u64>,
);
/// TxPoolCompliment
/// `TxPoolCompliment`
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GetTxPoolCompliment {
/// Tx Hashes

View file

@ -8,7 +8,6 @@ authors = ["Boog900"]
[dependencies]
cuprate-pruning = { path = "../../pruning" }
cuprate-wire = { path= "../../net/wire" }
cuprate-p2p-core = { path = "../p2p-core" }
tower = { workspace = true, features = ["util"] }
@ -29,3 +28,6 @@ borsh = { workspace = true, features = ["derive", "std"]}
cuprate-test-utils = {path = "../../test-utils"}
tokio = { workspace = true, features = ["rt-multi-thread", "macros"]}
[lints]
workspace = true

View file

@ -36,7 +36,7 @@ use crate::{
mod tests;
/// An entry in the connected list.
pub struct ConnectionPeerEntry<Z: NetworkZone> {
pub(crate) struct ConnectionPeerEntry<Z: NetworkZone> {
addr: Option<Z::Addr>,
id: u64,
handle: ConnectionHandle,
@ -109,14 +109,14 @@ impl<Z: BorshNetworkZone> AddressBook<Z> {
match handle.poll_unpin(cx) {
Poll::Pending => return,
Poll::Ready(Ok(Err(e))) => {
tracing::error!("Could not save peer list to disk, got error: {}", e)
tracing::error!("Could not save peer list to disk, got error: {e}");
}
Poll::Ready(Err(e)) => {
if e.is_panic() {
panic::resume_unwind(e.into_panic())
}
}
_ => (),
Poll::Ready(_) => (),
}
}
// the task is finished.
@ -144,6 +144,7 @@ impl<Z: BorshNetworkZone> AddressBook<Z> {
let mut internal_addr_disconnected = Vec::new();
let mut addrs_to_ban = Vec::new();
#[expect(clippy::iter_over_hash_type, reason = "ordering doesn't matter here")]
for (internal_addr, peer) in &mut self.connected_peers {
if let Some(time) = peer.handle.check_should_ban() {
match internal_addr {
@ -158,7 +159,7 @@ impl<Z: BorshNetworkZone> AddressBook<Z> {
}
}
for (addr, time) in addrs_to_ban.into_iter() {
for (addr, time) in addrs_to_ban {
self.ban_peer(addr, time);
}
@ -172,12 +173,7 @@ impl<Z: BorshNetworkZone> AddressBook<Z> {
.remove(&addr);
// If the amount of peers with this ban id is 0 remove the whole set.
if self
.connected_peers_ban_id
.get(&addr.ban_id())
.unwrap()
.is_empty()
{
if self.connected_peers_ban_id[&addr.ban_id()].is_empty() {
self.connected_peers_ban_id.remove(&addr.ban_id());
}
// remove the peer from the anchor list.
@ -188,7 +184,7 @@ impl<Z: BorshNetworkZone> AddressBook<Z> {
fn ban_peer(&mut self, addr: Z::Addr, time: Duration) {
if self.banned_peers.contains_key(&addr.ban_id()) {
tracing::error!("Tried to ban peer twice, this shouldn't happen.")
tracing::error!("Tried to ban peer twice, this shouldn't happen.");
}
if let Some(connected_peers_with_ban_id) = self.connected_peers_ban_id.get(&addr.ban_id()) {
@ -242,10 +238,10 @@ impl<Z: BorshNetworkZone> AddressBook<Z> {
peer_list.retain_mut(|peer| {
peer.adr.make_canonical();
if !peer.adr.should_add_to_peer_list() {
false
} else {
if peer.adr.should_add_to_peer_list() {
!self.is_peer_banned(&peer.adr)
} else {
false
}
// TODO: check rpc/ p2p ports not the same
});
@ -391,7 +387,7 @@ impl<Z: BorshNetworkZone> Service<AddressBookRequest<Z>> for AddressBook<Z> {
rpc_credits_per_hash,
},
)
.map(|_| AddressBookResponse::Ok),
.map(|()| AddressBookResponse::Ok),
AddressBookRequest::IncomingPeerList(peer_list) => {
self.handle_incoming_peer_list(peer_list);
Ok(AddressBookResponse::Ok)

View file

@ -109,7 +109,7 @@ async fn add_new_peer_already_connected() {
},
),
Err(AddressBookError::PeerAlreadyConnected)
)
);
}
#[tokio::test]
@ -143,5 +143,5 @@ async fn banned_peer_removed_from_peer_lists() {
.unwrap()
.into_inner(),
TestNetZoneAddr(1)
)
);
}

View file

@ -7,31 +7,31 @@ use cuprate_p2p_core::{services::ZoneSpecificPeerListEntryBase, NetZoneAddress,
use cuprate_pruning::{PruningSeed, CRYPTONOTE_MAX_BLOCK_HEIGHT};
#[cfg(test)]
pub mod tests;
pub(crate) mod tests;
/// A Peer list in the address book.
///
/// This could either be the white list or gray list.
#[derive(Debug)]
pub struct PeerList<Z: NetworkZone> {
pub(crate) struct PeerList<Z: NetworkZone> {
/// The peers with their peer data.
pub peers: IndexMap<Z::Addr, ZoneSpecificPeerListEntryBase<Z::Addr>>,
/// An index of Pruning seed to address, so can quickly grab peers with the blocks
/// we want.
///
/// Pruning seeds are sorted by first their log_stripes and then their stripe.
/// Pruning seeds are sorted by first their `log_stripes` and then their stripe.
/// This means the first peers in this list will store more blocks than peers
/// later on. So when we need a peer with a certain block we look at the peers
/// storing more blocks first then work our way to the peers storing less.
///
pruning_seeds: BTreeMap<PruningSeed, Vec<Z::Addr>>,
/// A hashmap linking ban_ids to addresses.
/// A hashmap linking `ban_ids` to addresses.
ban_ids: HashMap<<Z::Addr as NetZoneAddress>::BanID, Vec<Z::Addr>>,
}
impl<Z: NetworkZone> PeerList<Z> {
/// Creates a new peer list.
pub fn new(list: Vec<ZoneSpecificPeerListEntryBase<Z::Addr>>) -> PeerList<Z> {
pub(crate) fn new(list: Vec<ZoneSpecificPeerListEntryBase<Z::Addr>>) -> Self {
let mut peers = IndexMap::with_capacity(list.len());
let mut pruning_seeds = BTreeMap::new();
let mut ban_ids = HashMap::with_capacity(list.len());
@ -49,7 +49,7 @@ impl<Z: NetworkZone> PeerList<Z> {
peers.insert(peer.adr, peer);
}
PeerList {
Self {
peers,
pruning_seeds,
ban_ids,
@ -57,21 +57,20 @@ impl<Z: NetworkZone> PeerList<Z> {
}
/// Gets the length of the peer list
pub fn len(&self) -> usize {
pub(crate) fn len(&self) -> usize {
self.peers.len()
}
/// Adds a new peer to the peer list
pub fn add_new_peer(&mut self, peer: ZoneSpecificPeerListEntryBase<Z::Addr>) {
pub(crate) fn add_new_peer(&mut self, peer: ZoneSpecificPeerListEntryBase<Z::Addr>) {
if self.peers.insert(peer.adr, peer).is_none() {
// It's more clear with this
#[allow(clippy::unwrap_or_default)]
#[expect(clippy::unwrap_or_default, reason = "It's more clear with this")]
self.pruning_seeds
.entry(peer.pruning_seed)
.or_insert_with(Vec::new)
.push(peer.adr);
#[allow(clippy::unwrap_or_default)]
#[expect(clippy::unwrap_or_default)]
self.ban_ids
.entry(peer.adr.ban_id())
.or_insert_with(Vec::new)
@ -85,7 +84,7 @@ impl<Z: NetworkZone> PeerList<Z> {
/// list.
///
/// The given peer will be removed from the peer list.
pub fn take_random_peer<R: Rng>(
pub(crate) fn take_random_peer<R: Rng>(
&mut self,
r: &mut R,
block_needed: Option<usize>,
@ -127,7 +126,7 @@ impl<Z: NetworkZone> PeerList<Z> {
None
}
pub fn get_random_peers<R: Rng>(
pub(crate) fn get_random_peers<R: Rng>(
&self,
r: &mut R,
len: usize,
@ -142,7 +141,7 @@ impl<Z: NetworkZone> PeerList<Z> {
}
/// Returns a mutable reference to a peer.
pub fn get_peer_mut(
pub(crate) fn get_peer_mut(
&mut self,
peer: &Z::Addr,
) -> Option<&mut ZoneSpecificPeerListEntryBase<Z::Addr>> {
@ -150,7 +149,7 @@ impl<Z: NetworkZone> PeerList<Z> {
}
/// Returns true if the list contains this peer.
pub fn contains_peer(&self, peer: &Z::Addr) -> bool {
pub(crate) fn contains_peer(&self, peer: &Z::Addr) -> bool {
self.peers.contains_key(peer)
}
@ -189,11 +188,11 @@ impl<Z: NetworkZone> PeerList<Z> {
/// MUST NOT BE USED ALONE
fn remove_peer_from_all_idxs(&mut self, peer: &ZoneSpecificPeerListEntryBase<Z::Addr>) {
self.remove_peer_pruning_idx(peer);
self.remove_peer_ban_idx(peer)
self.remove_peer_ban_idx(peer);
}
/// Removes a peer from the peer list
pub fn remove_peer(
pub(crate) fn remove_peer(
&mut self,
peer: &Z::Addr,
) -> Option<ZoneSpecificPeerListEntryBase<Z::Addr>> {
@ -203,7 +202,7 @@ impl<Z: NetworkZone> PeerList<Z> {
}
/// Removes all peers with a specific ban id.
pub fn remove_peers_with_ban_id(&mut self, ban_id: &<Z::Addr as NetZoneAddress>::BanID) {
pub(crate) fn remove_peers_with_ban_id(&mut self, ban_id: &<Z::Addr as NetZoneAddress>::BanID) {
let Some(addresses) = self.ban_ids.get(ban_id) else {
// No peers to ban
return;
@ -217,8 +216,8 @@ impl<Z: NetworkZone> PeerList<Z> {
/// Tries to reduce the peer list to `new_len`.
///
/// This function could keep the list bigger than `new_len` if `must_keep_peers`s length
/// is larger than new_len, in that case we will remove as much as we can.
pub fn reduce_list(&mut self, must_keep_peers: &HashSet<Z::Addr>, new_len: usize) {
/// is larger than `new_len`, in that case we will remove as much as we can.
pub(crate) fn reduce_list(&mut self, must_keep_peers: &HashSet<Z::Addr>, new_len: usize) {
if new_len >= self.len() {
return;
}

View file

@ -14,7 +14,7 @@ fn make_fake_peer(
) -> ZoneSpecificPeerListEntryBase<TestNetZoneAddr> {
ZoneSpecificPeerListEntryBase {
adr: TestNetZoneAddr(id),
id: id as u64,
id: u64::from(id),
last_seen: 0,
pruning_seed: PruningSeed::decompress(pruning_seed.unwrap_or(0)).unwrap(),
rpc_port: 0,
@ -22,14 +22,14 @@ fn make_fake_peer(
}
}
pub fn make_fake_peer_list(
pub(crate) fn make_fake_peer_list(
start_idx: u32,
numb_o_peers: u32,
) -> PeerList<TestNetZone<true, true, true>> {
let mut peer_list = Vec::with_capacity(numb_o_peers as usize);
for idx in start_idx..(start_idx + numb_o_peers) {
peer_list.push(make_fake_peer(idx, None))
peer_list.push(make_fake_peer(idx, None));
}
PeerList::new(peer_list)
@ -50,7 +50,7 @@ fn make_fake_peer_list_with_random_pruning_seeds(
} else {
r.gen_range(384..=391)
}),
))
));
}
PeerList::new(peer_list)
}
@ -70,7 +70,7 @@ fn peer_list_reduce_length() {
#[test]
fn peer_list_reduce_length_with_peers_we_need() {
let mut peer_list = make_fake_peer_list(0, 500);
let must_keep_peers = HashSet::from_iter(peer_list.peers.keys().copied());
let must_keep_peers = peer_list.peers.keys().copied().collect::<HashSet<_>>();
let target_len = 49;
@ -92,7 +92,7 @@ fn peer_list_remove_specific_peer() {
let peers = peer_list.peers;
for (_, addrs) in pruning_idxs {
addrs.iter().for_each(|adr| assert_ne!(adr, &peer.adr))
addrs.iter().for_each(|adr| assert_ne!(adr, &peer.adr));
}
assert!(!peers.contains_key(&peer.adr));
@ -104,13 +104,13 @@ fn peer_list_pruning_idxs_are_correct() {
let mut total_len = 0;
for (seed, list) in peer_list.pruning_seeds {
for peer in list.iter() {
for peer in &list {
assert_eq!(peer_list.peers.get(peer).unwrap().pruning_seed, seed);
total_len += 1;
}
}
assert_eq!(total_len, peer_list.peers.len())
assert_eq!(total_len, peer_list.peers.len());
}
#[test]
@ -122,11 +122,7 @@ fn peer_list_add_new_peer() {
assert_eq!(peer_list.len(), 11);
assert_eq!(peer_list.peers.get(&new_peer.adr), Some(&new_peer));
assert!(peer_list
.pruning_seeds
.get(&new_peer.pruning_seed)
.unwrap()
.contains(&new_peer.adr));
assert!(peer_list.pruning_seeds[&new_peer.pruning_seed].contains(&new_peer.adr));
}
#[test]
@ -164,7 +160,7 @@ fn peer_list_get_peer_with_block() {
assert!(peer
.pruning_seed
.get_next_unpruned_block(1, 1_000_000)
.is_ok())
.is_ok());
}
#[test]

View file

@ -1,3 +1,8 @@
#![expect(
single_use_lifetimes,
reason = "false positive on generated derive code on `SerPeerDataV1`"
)]
use std::fs;
use borsh::{from_slice, to_vec, BorshDeserialize, BorshSerialize};
@ -21,7 +26,7 @@ struct DeserPeerDataV1<A: NetZoneAddress> {
gray_list: Vec<ZoneSpecificPeerListEntryBase<A>>,
}
pub fn save_peers_to_disk<Z: BorshNetworkZone>(
pub(crate) fn save_peers_to_disk<Z: BorshNetworkZone>(
cfg: &AddressBookConfig,
white_list: &PeerList<Z>,
gray_list: &PeerList<Z>,
@ -38,7 +43,7 @@ pub fn save_peers_to_disk<Z: BorshNetworkZone>(
spawn_blocking(move || fs::write(&file, &data))
}
pub async fn read_peers_from_disk<Z: BorshNetworkZone>(
pub(crate) async fn read_peers_from_disk<Z: BorshNetworkZone>(
cfg: &AddressBookConfig,
) -> Result<
(

View file

@ -13,3 +13,6 @@ borsh = ["dep:borsh"]
thiserror = { workspace = true }
borsh = { workspace = true, features = ["derive", "std"], optional = true }
[lints]
workspace = true

View file

@ -71,7 +71,7 @@ impl PruningSeed {
///
/// See: [`DecompressedPruningSeed::new`]
pub fn new_pruned(stripe: u32, log_stripes: u32) -> Result<Self, PruningError> {
Ok(PruningSeed::Pruned(DecompressedPruningSeed::new(
Ok(Self::Pruned(DecompressedPruningSeed::new(
stripe,
log_stripes,
)?))
@ -81,9 +81,7 @@ impl PruningSeed {
///
/// An error means the pruning seed was invalid.
pub fn decompress(seed: u32) -> Result<Self, PruningError> {
Ok(DecompressedPruningSeed::decompress(seed)?
.map(PruningSeed::Pruned)
.unwrap_or(PruningSeed::NotPruned))
Ok(DecompressedPruningSeed::decompress(seed)?.map_or(Self::NotPruned, Self::Pruned))
}
/// Decompresses the seed, performing the same checks as [`PruningSeed::decompress`] and some more according to
@ -103,34 +101,34 @@ impl PruningSeed {
}
/// Compresses this pruning seed to a u32.
pub fn compress(&self) -> u32 {
pub const fn compress(&self) -> u32 {
match self {
PruningSeed::NotPruned => 0,
PruningSeed::Pruned(seed) => seed.compress(),
Self::NotPruned => 0,
Self::Pruned(seed) => seed.compress(),
}
}
/// Returns the `log_stripes` for this seed, if this seed is pruned otherwise [`None`] is returned.
pub fn get_log_stripes(&self) -> Option<u32> {
pub const fn get_log_stripes(&self) -> Option<u32> {
match self {
PruningSeed::NotPruned => None,
PruningSeed::Pruned(seed) => Some(seed.log_stripes),
Self::NotPruned => None,
Self::Pruned(seed) => Some(seed.log_stripes),
}
}
/// Returns the `stripe` for this seed, if this seed is pruned otherwise [`None`] is returned.
pub fn get_stripe(&self) -> Option<u32> {
pub const fn get_stripe(&self) -> Option<u32> {
match self {
PruningSeed::NotPruned => None,
PruningSeed::Pruned(seed) => Some(seed.stripe),
Self::NotPruned => None,
Self::Pruned(seed) => Some(seed.stripe),
}
}
/// Returns `true` if a peer with this pruning seed should have a non-pruned version of a block.
pub fn has_full_block(&self, height: usize, blockchain_height: usize) -> bool {
pub const fn has_full_block(&self, height: usize, blockchain_height: usize) -> bool {
match self {
PruningSeed::NotPruned => true,
PruningSeed::Pruned(seed) => seed.has_full_block(height, blockchain_height),
Self::NotPruned => true,
Self::Pruned(seed) => seed.has_full_block(height, blockchain_height),
}
}
@ -155,10 +153,8 @@ impl PruningSeed {
blockchain_height: usize,
) -> Result<Option<usize>, PruningError> {
Ok(match self {
PruningSeed::NotPruned => None,
PruningSeed::Pruned(seed) => {
seed.get_next_pruned_block(block_height, blockchain_height)?
}
Self::NotPruned => None,
Self::Pruned(seed) => seed.get_next_pruned_block(block_height, blockchain_height)?,
})
}
@ -181,10 +177,8 @@ impl PruningSeed {
blockchain_height: usize,
) -> Result<usize, PruningError> {
Ok(match self {
PruningSeed::NotPruned => block_height,
PruningSeed::Pruned(seed) => {
seed.get_next_unpruned_block(block_height, blockchain_height)?
}
Self::NotPruned => block_height,
Self::Pruned(seed) => seed.get_next_unpruned_block(block_height, blockchain_height)?,
})
}
}
@ -199,11 +193,11 @@ impl Ord for PruningSeed {
fn cmp(&self, other: &Self) -> Ordering {
match (self, other) {
// Make sure pruning seeds storing more blocks are greater.
(PruningSeed::NotPruned, PruningSeed::NotPruned) => Ordering::Equal,
(PruningSeed::NotPruned, PruningSeed::Pruned(_)) => Ordering::Greater,
(PruningSeed::Pruned(_), PruningSeed::NotPruned) => Ordering::Less,
(Self::NotPruned, Self::NotPruned) => Ordering::Equal,
(Self::NotPruned, Self::Pruned(_)) => Ordering::Greater,
(Self::Pruned(_), Self::NotPruned) => Ordering::Less,
(PruningSeed::Pruned(seed1), PruningSeed::Pruned(seed2)) => seed1.cmp(seed2),
(Self::Pruned(seed1), Self::Pruned(seed2)) => seed1.cmp(seed2),
}
}
}
@ -222,7 +216,7 @@ pub struct DecompressedPruningSeed {
log_stripes: u32,
/// The specific portion this peer keeps.
///
/// *MUST* be between 1..=2^log_stripes
/// *MUST* be between `1..=2^log_stripes`
stripe: u32,
}
@ -268,13 +262,13 @@ impl DecompressedPruningSeed {
/// a valid seed you currently MUST pass in a number 1 to 8 for `stripe`
/// and 3 for `log_stripes`.*
///
pub fn new(stripe: u32, log_stripes: u32) -> Result<Self, PruningError> {
pub const fn new(stripe: u32, log_stripes: u32) -> Result<Self, PruningError> {
if log_stripes > PRUNING_SEED_LOG_STRIPES_MASK {
Err(PruningError::LogStripesOutOfRange)
} else if !(stripe > 0 && stripe <= (1 << log_stripes)) {
Err(PruningError::StripeOutOfRange)
} else {
Ok(DecompressedPruningSeed {
Ok(Self {
log_stripes,
stripe,
})
@ -286,7 +280,7 @@ impl DecompressedPruningSeed {
/// Will return Ok(None) if the pruning seed means no pruning.
///
/// An error means the pruning seed was invalid.
pub fn decompress(seed: u32) -> Result<Option<Self>, PruningError> {
pub const fn decompress(seed: u32) -> Result<Option<Self>, PruningError> {
if seed == 0 {
// No pruning.
return Ok(None);
@ -299,20 +293,20 @@ impl DecompressedPruningSeed {
return Err(PruningError::StripeOutOfRange);
}
Ok(Some(DecompressedPruningSeed {
Ok(Some(Self {
log_stripes,
stripe,
}))
}
/// Compresses the pruning seed into a u32.
pub fn compress(&self) -> u32 {
pub const fn compress(&self) -> u32 {
(self.log_stripes << PRUNING_SEED_LOG_STRIPES_SHIFT)
| ((self.stripe - 1) << PRUNING_SEED_STRIPE_SHIFT)
}
/// Returns `true` if a peer with this pruning seed should have a non-pruned version of a block.
pub fn has_full_block(&self, height: usize, blockchain_height: usize) -> bool {
pub const fn has_full_block(&self, height: usize, blockchain_height: usize) -> bool {
match get_block_pruning_stripe(height, blockchain_height, self.log_stripes) {
Some(block_stripe) => self.stripe == block_stripe,
None => true,
@ -419,7 +413,7 @@ impl DecompressedPruningSeed {
// We can get the end of our "non-pruning" cycle by getting the next stripe's first un-pruned block height.
// So we calculate the next un-pruned block for the next stripe and return it as our next pruned block
let next_stripe = 1 + (self.stripe & ((1 << self.log_stripes) - 1));
let seed = DecompressedPruningSeed::new(next_stripe, self.log_stripes)
let seed = Self::new(next_stripe, self.log_stripes)
.expect("We just made sure this stripe is in range for this log_stripe");
let calculated_height = seed.get_next_unpruned_block(block_height, blockchain_height)?;
@ -433,7 +427,7 @@ impl DecompressedPruningSeed {
}
}
fn get_block_pruning_stripe(
const fn get_block_pruning_stripe(
block_height: usize,
blockchain_height: usize,
log_stripe: u32,
@ -441,9 +435,14 @@ fn get_block_pruning_stripe(
if block_height + CRYPTONOTE_PRUNING_TIP_BLOCKS >= blockchain_height {
None
} else {
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "it's trivial to prove it's ok to us `as` here"
)]
Some(
(((block_height / CRYPTONOTE_PRUNING_STRIPE_SIZE) & ((1 << log_stripe) as usize - 1))
+ 1) as u32, // it's trivial to prove it's ok to us `as` here
+ 1) as u32,
)
}
}
@ -483,16 +482,17 @@ mod tests {
#[test]
fn get_pruning_log_stripe() {
let all_valid_seeds = make_all_pruning_seeds();
for seed in all_valid_seeds.iter() {
assert_eq!(seed.get_log_stripes().unwrap(), 3)
for seed in &all_valid_seeds {
assert_eq!(seed.get_log_stripes().unwrap(), 3);
}
}
#[test]
fn get_pruning_stripe() {
let all_valid_seeds = make_all_pruning_seeds();
#[expect(clippy::cast_possible_truncation)]
for (i, seed) in all_valid_seeds.iter().enumerate() {
assert_eq!(seed.get_stripe().unwrap(), i as u32 + 1)
assert_eq!(seed.get_stripe().unwrap(), i as u32 + 1);
}
}
@ -554,7 +554,7 @@ mod tests {
assert_eq!(
seed.get_next_unpruned_block(0, blockchain_height).unwrap(),
i * 4096
)
);
}
for (i, seed) in all_valid_seeds.iter().enumerate() {
@ -562,7 +562,7 @@ mod tests {
seed.get_next_unpruned_block((i + 1) * 4096, blockchain_height)
.unwrap(),
i * 4096 + 32768
)
);
}
for (i, seed) in all_valid_seeds.iter().enumerate() {
@ -570,15 +570,15 @@ mod tests {
seed.get_next_unpruned_block((i + 8) * 4096, blockchain_height)
.unwrap(),
i * 4096 + 32768
)
);
}
for seed in all_valid_seeds.iter() {
for seed in &all_valid_seeds {
assert_eq!(
seed.get_next_unpruned_block(76437863 - 1, blockchain_height)
.unwrap(),
76437863 - 1
)
);
}
let zero_seed = PruningSeed::NotPruned;
@ -591,7 +591,7 @@ mod tests {
let seed = PruningSeed::decompress(384).unwrap();
// the next unpruned block is the first tip block
assert_eq!(seed.get_next_unpruned_block(5000, 11000).unwrap(), 5500)
assert_eq!(seed.get_next_unpruned_block(5000, 11000).unwrap(), 5500);
}
#[test]
@ -605,7 +605,7 @@ mod tests {
.unwrap()
.unwrap(),
0
)
);
}
for (i, seed) in all_valid_seeds.iter().enumerate() {
@ -614,7 +614,7 @@ mod tests {
.unwrap()
.unwrap(),
(i + 1) * 4096
)
);
}
for (i, seed) in all_valid_seeds.iter().enumerate() {
@ -623,15 +623,15 @@ mod tests {
.unwrap()
.unwrap(),
(i + 9) * 4096
)
);
}
for seed in all_valid_seeds.iter() {
for seed in &all_valid_seeds {
assert_eq!(
seed.get_next_pruned_block(76437863 - 1, blockchain_height)
.unwrap(),
None
)
);
}
let zero_seed = PruningSeed::NotPruned;
@ -644,6 +644,6 @@ mod tests {
let seed = PruningSeed::decompress(384).unwrap();
// there is no next pruned block
assert_eq!(seed.get_next_pruned_block(5000, 10000).unwrap(), None)
assert_eq!(seed.get_next_pruned_block(5000, 10000).unwrap(), None);
}
}

View file

@ -15,21 +15,19 @@ default = ["heed", "service"]
heed = ["cuprate-database/heed"]
redb = ["cuprate-database/redb"]
redb-memory = ["cuprate-database/redb-memory"]
service = ["dep:thread_local", "dep:rayon"]
service = ["dep:thread_local", "dep:rayon", "cuprate-helper/thread"]
[dependencies]
# FIXME:
# We only need the `thread` feature if `service` is enabled.
# Figure out how to enable features of an already pulled in dependency conditionally.
cuprate-database = { path = "../database" }
cuprate-database-service = { path = "../service" }
cuprate-helper = { path = "../../helper", features = ["fs", "thread", "map"] }
cuprate-helper = { path = "../../helper", features = ["fs", "map"] }
cuprate-types = { path = "../../types", features = ["blockchain"] }
cuprate-pruning = { path = "../../pruning" }
bitflags = { workspace = true, features = ["std", "serde", "bytemuck"] }
bytemuck = { workspace = true, features = ["must_cast", "derive", "min_const_generics", "extern_crate_alloc"] }
curve25519-dalek = { workspace = true }
cuprate-pruning = { path = "../../pruning" }
rand = { workspace = true }
monero-serai = { workspace = true, features = ["std"] }
serde = { workspace = true, optional = true }

View file

@ -0,0 +1,337 @@
use bytemuck::TransparentWrapper;
use monero_serai::block::{Block, BlockHeader};
use cuprate_database::{DatabaseRo, DatabaseRw, RuntimeError, StorableVec};
use cuprate_helper::map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits};
use cuprate_types::{AltBlockInformation, Chain, ChainId, ExtendedBlockHeader, HardFork};
use crate::{
ops::{
alt_block::{add_alt_transaction_blob, get_alt_transaction, update_alt_chain_info},
block::get_block_info,
macros::doc_error,
},
tables::{Tables, TablesMut},
types::{AltBlockHeight, BlockHash, BlockHeight, CompactAltBlockInfo},
};
/// Flush all alt-block data from all the alt-block tables.
///
/// This function completely empties the alt block tables.
pub fn flush_alt_blocks<'a, E: cuprate_database::EnvInner<'a>>(
env_inner: &E,
tx_rw: &mut E::Rw<'_>,
) -> Result<(), RuntimeError> {
use crate::tables::{
AltBlockBlobs, AltBlockHeights, AltBlocksInfo, AltChainInfos, AltTransactionBlobs,
AltTransactionInfos,
};
env_inner.clear_db::<AltChainInfos>(tx_rw)?;
env_inner.clear_db::<AltBlockHeights>(tx_rw)?;
env_inner.clear_db::<AltBlocksInfo>(tx_rw)?;
env_inner.clear_db::<AltBlockBlobs>(tx_rw)?;
env_inner.clear_db::<AltTransactionBlobs>(tx_rw)?;
env_inner.clear_db::<AltTransactionInfos>(tx_rw)
}
/// Add a [`AltBlockInformation`] to the database.
///
/// This extracts all the data from the input block and
/// maps/adds them to the appropriate database tables.
///
#[doc = doc_error!()]
///
/// # Panics
/// This function will panic if:
/// - `alt_block.height` is == `0`
/// - `alt_block.txs.len()` != `alt_block.block.transactions.len()`
///
pub fn add_alt_block(
alt_block: &AltBlockInformation,
tables: &mut impl TablesMut,
) -> Result<(), RuntimeError> {
let alt_block_height = AltBlockHeight {
chain_id: alt_block.chain_id.into(),
height: alt_block.height,
};
tables
.alt_block_heights_mut()
.put(&alt_block.block_hash, &alt_block_height)?;
update_alt_chain_info(&alt_block_height, &alt_block.block.header.previous, tables)?;
let (cumulative_difficulty_low, cumulative_difficulty_high) =
split_u128_into_low_high_bits(alt_block.cumulative_difficulty);
let alt_block_info = CompactAltBlockInfo {
block_hash: alt_block.block_hash,
pow_hash: alt_block.pow_hash,
height: alt_block.height,
weight: alt_block.weight,
long_term_weight: alt_block.long_term_weight,
cumulative_difficulty_low,
cumulative_difficulty_high,
};
tables
.alt_blocks_info_mut()
.put(&alt_block_height, &alt_block_info)?;
tables.alt_block_blobs_mut().put(
&alt_block_height,
StorableVec::wrap_ref(&alt_block.block_blob),
)?;
assert_eq!(alt_block.txs.len(), alt_block.block.transactions.len());
for tx in &alt_block.txs {
add_alt_transaction_blob(tx, tables)?;
}
Ok(())
}
/// Retrieves an [`AltBlockInformation`] from the database.
///
/// This function will look at only the blocks with the given [`AltBlockHeight::chain_id`], no others
/// even if they are technically part of this chain.
#[doc = doc_error!()]
pub fn get_alt_block(
alt_block_height: &AltBlockHeight,
tables: &impl Tables,
) -> Result<AltBlockInformation, RuntimeError> {
let block_info = tables.alt_blocks_info().get(alt_block_height)?;
let block_blob = tables.alt_block_blobs().get(alt_block_height)?.0;
let block = Block::read(&mut block_blob.as_slice())?;
let txs = block
.transactions
.iter()
.map(|tx_hash| get_alt_transaction(tx_hash, tables))
.collect::<Result<_, RuntimeError>>()?;
Ok(AltBlockInformation {
block,
block_blob,
txs,
block_hash: block_info.block_hash,
pow_hash: block_info.pow_hash,
height: block_info.height,
weight: block_info.weight,
long_term_weight: block_info.long_term_weight,
cumulative_difficulty: combine_low_high_bits_to_u128(
block_info.cumulative_difficulty_low,
block_info.cumulative_difficulty_high,
),
chain_id: alt_block_height.chain_id.into(),
})
}
/// Retrieves the hash of the block at the given `block_height` on the alt chain with
/// the given [`ChainId`].
///
/// This function will get blocks from the whole chain, for example if you were to ask for height
/// `0` with any [`ChainId`] (as long that chain actually exists) you will get the main chain genesis.
///
#[doc = doc_error!()]
pub fn get_alt_block_hash(
block_height: &BlockHeight,
alt_chain: ChainId,
tables: &impl Tables,
) -> Result<BlockHash, RuntimeError> {
let alt_chains = tables.alt_chain_infos();
// First find what [`ChainId`] this block would be stored under.
let original_chain = {
let mut chain = alt_chain.into();
loop {
let chain_info = alt_chains.get(&chain)?;
if chain_info.common_ancestor_height < *block_height {
break Chain::Alt(chain.into());
}
match chain_info.parent_chain.into() {
Chain::Main => break Chain::Main,
Chain::Alt(alt_chain_id) => {
chain = alt_chain_id.into();
continue;
}
}
}
};
// Get the block hash.
match original_chain {
Chain::Main => {
get_block_info(block_height, tables.block_infos()).map(|info| info.block_hash)
}
Chain::Alt(chain_id) => tables
.alt_blocks_info()
.get(&AltBlockHeight {
chain_id: chain_id.into(),
height: *block_height,
})
.map(|info| info.block_hash),
}
}
/// Retrieves the [`ExtendedBlockHeader`] of the alt-block with an exact [`AltBlockHeight`].
///
/// This function will look at only the blocks with the given [`AltBlockHeight::chain_id`], no others
/// even if they are technically part of this chain.
///
#[doc = doc_error!()]
pub fn get_alt_block_extended_header_from_height(
height: &AltBlockHeight,
table: &impl Tables,
) -> Result<ExtendedBlockHeader, RuntimeError> {
let block_info = table.alt_blocks_info().get(height)?;
let block_blob = table.alt_block_blobs().get(height)?.0;
let block_header = BlockHeader::read(&mut block_blob.as_slice())?;
Ok(ExtendedBlockHeader {
version: HardFork::from_version(block_header.hardfork_version)
.expect("Block in DB must have correct version"),
vote: block_header.hardfork_version,
timestamp: block_header.timestamp,
cumulative_difficulty: combine_low_high_bits_to_u128(
block_info.cumulative_difficulty_low,
block_info.cumulative_difficulty_high,
),
block_weight: block_info.weight,
long_term_weight: block_info.long_term_weight,
})
}
#[cfg(test)]
mod tests {
use std::num::NonZero;
use cuprate_database::{Env, EnvInner, TxRw};
use cuprate_test_utils::data::{BLOCK_V16_TX0, BLOCK_V1_TX2, BLOCK_V9_TX3};
use cuprate_types::{Chain, ChainId};
use crate::{
ops::{
alt_block::{
add_alt_block, flush_alt_blocks, get_alt_block,
get_alt_block_extended_header_from_height, get_alt_block_hash,
get_alt_chain_history_ranges,
},
block::{add_block, pop_block},
},
tables::{OpenTables, Tables},
tests::{assert_all_tables_are_empty, map_verified_block_to_alt, tmp_concrete_env},
types::AltBlockHeight,
};
#[expect(clippy::range_plus_one)]
#[test]
fn all_alt_blocks() {
let (env, _tmp) = tmp_concrete_env();
let env_inner = env.env_inner();
assert_all_tables_are_empty(&env);
let chain_id = ChainId(NonZero::new(1).unwrap());
// Add initial block.
{
let tx_rw = env_inner.tx_rw().unwrap();
let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
let mut initial_block = BLOCK_V1_TX2.clone();
initial_block.height = 0;
add_block(&initial_block, &mut tables).unwrap();
drop(tables);
TxRw::commit(tx_rw).unwrap();
}
let alt_blocks = [
map_verified_block_to_alt(BLOCK_V9_TX3.clone(), chain_id),
map_verified_block_to_alt(BLOCK_V16_TX0.clone(), chain_id),
];
// Add alt-blocks
{
let tx_rw = env_inner.tx_rw().unwrap();
let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
let mut prev_hash = BLOCK_V1_TX2.block_hash;
for (i, mut alt_block) in alt_blocks.into_iter().enumerate() {
let height = i + 1;
alt_block.height = height;
alt_block.block.header.previous = prev_hash;
alt_block.block_blob = alt_block.block.serialize();
add_alt_block(&alt_block, &mut tables).unwrap();
let alt_height = AltBlockHeight {
chain_id: chain_id.into(),
height,
};
let alt_block_2 = get_alt_block(&alt_height, &tables).unwrap();
assert_eq!(alt_block.block, alt_block_2.block);
let headers = get_alt_chain_history_ranges(
0..(height + 1),
chain_id,
tables.alt_chain_infos(),
)
.unwrap();
assert_eq!(headers.len(), 2);
assert_eq!(headers[1], (Chain::Main, 0..1));
assert_eq!(headers[0], (Chain::Alt(chain_id), 1..(height + 1)));
prev_hash = alt_block.block_hash;
let header =
get_alt_block_extended_header_from_height(&alt_height, &tables).unwrap();
assert_eq!(header.timestamp, alt_block.block.header.timestamp);
assert_eq!(header.block_weight, alt_block.weight);
assert_eq!(header.long_term_weight, alt_block.long_term_weight);
assert_eq!(
header.cumulative_difficulty,
alt_block.cumulative_difficulty
);
assert_eq!(
header.version.as_u8(),
alt_block.block.header.hardfork_version
);
assert_eq!(header.vote, alt_block.block.header.hardfork_signal);
let block_hash = get_alt_block_hash(&height, chain_id, &tables).unwrap();
assert_eq!(block_hash, alt_block.block_hash);
}
drop(tables);
TxRw::commit(tx_rw).unwrap();
}
{
let mut tx_rw = env_inner.tx_rw().unwrap();
flush_alt_blocks(&env_inner, &mut tx_rw).unwrap();
let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
pop_block(None, &mut tables).unwrap();
drop(tables);
TxRw::commit(tx_rw).unwrap();
}
assert_all_tables_are_empty(&env);
}
}

View file

@ -0,0 +1,117 @@
use std::cmp::{max, min};
use cuprate_database::{DatabaseRo, DatabaseRw, RuntimeError};
use cuprate_types::{Chain, ChainId};
use crate::{
ops::macros::{doc_add_alt_block_inner_invariant, doc_error},
tables::{AltChainInfos, TablesMut},
types::{AltBlockHeight, AltChainInfo, BlockHash, BlockHeight},
};
/// Updates the [`AltChainInfo`] with information on a new alt-block.
///
#[doc = doc_add_alt_block_inner_invariant!()]
#[doc = doc_error!()]
///
/// # Panics
///
/// This will panic if [`AltBlockHeight::height`] == `0`.
pub fn update_alt_chain_info(
alt_block_height: &AltBlockHeight,
prev_hash: &BlockHash,
tables: &mut impl TablesMut,
) -> Result<(), RuntimeError> {
let parent_chain = match tables.alt_block_heights().get(prev_hash) {
Ok(alt_parent_height) => Chain::Alt(alt_parent_height.chain_id.into()),
Err(RuntimeError::KeyNotFound) => Chain::Main,
Err(e) => return Err(e),
};
// try update the info if one exists for this chain.
let update = tables
.alt_chain_infos_mut()
.update(&alt_block_height.chain_id, |mut info| {
if info.chain_height < alt_block_height.height + 1 {
// If the chain height is increasing we only need to update the chain height.
info.chain_height = alt_block_height.height + 1;
} else {
// If the chain height is not increasing we are popping blocks and need to update the
// split point.
info.common_ancestor_height = alt_block_height.height.checked_sub(1).unwrap();
info.parent_chain = parent_chain.into();
}
info.chain_height = alt_block_height.height + 1;
Some(info)
});
match update {
Ok(()) => return Ok(()),
Err(RuntimeError::KeyNotFound) => (),
Err(e) => return Err(e),
}
// If one doesn't already exist add it.
tables.alt_chain_infos_mut().put(
&alt_block_height.chain_id,
&AltChainInfo {
parent_chain: parent_chain.into(),
common_ancestor_height: alt_block_height.height.checked_sub(1).unwrap(),
chain_height: alt_block_height.height + 1,
},
)
}
/// Get the height history of an alt-chain in reverse chronological order.
///
/// Height history is a list of height ranges with the corresponding [`Chain`] they are stored under.
/// For example if your range goes from height `0` the last entry in the list will be [`Chain::Main`]
/// upto the height where the first split occurs.
#[doc = doc_error!()]
pub fn get_alt_chain_history_ranges(
range: std::ops::Range<BlockHeight>,
alt_chain: ChainId,
alt_chain_infos: &impl DatabaseRo<AltChainInfos>,
) -> Result<Vec<(Chain, std::ops::Range<BlockHeight>)>, RuntimeError> {
let mut ranges = Vec::with_capacity(5);
let mut i = range.end;
let mut current_chain_id = alt_chain.into();
while i > range.start {
let chain_info = alt_chain_infos.get(&current_chain_id)?;
let start_height = max(range.start, chain_info.common_ancestor_height + 1);
let end_height = min(i, chain_info.chain_height);
ranges.push((
Chain::Alt(current_chain_id.into()),
start_height..end_height,
));
i = chain_info.common_ancestor_height + 1;
match chain_info.parent_chain.into() {
Chain::Main => {
ranges.push((Chain::Main, range.start..i));
break;
}
Chain::Alt(alt_chain_id) => {
let alt_chain_id = alt_chain_id.into();
// This shouldn't be possible to hit, however in a test with custom (invalid) block data
// this caused an infinite loop.
if alt_chain_id == current_chain_id {
return Err(RuntimeError::Io(std::io::Error::other(
"Loop detected in ChainIDs, invalid alt chain.",
)));
}
current_chain_id = alt_chain_id;
continue;
}
}
}
Ok(ranges)
}

View file

@ -0,0 +1,58 @@
//! Alternative Block/Chain Ops
//!
//! Alternative chains are chains that potentially have more proof-of-work than the main-chain
//! which we are tracking to potentially re-org to.
//!
//! Cuprate uses an ID system for alt-chains. When a split is made from the main-chain we generate
//! a random [`ChainID`](cuprate_types::ChainId) and assign it to the chain:
//!
//! ```text
//! |
//! |
//! | split
//! |-------------
//! | |
//! | |
//! \|/ \|/
//! main-chain ChainID(X)
//! ```
//!
//! In that example if we were to receive an alt-block which immediately follows the top block of `ChainID(X)`
//! then that block will also be stored under `ChainID(X)`. However, if it follows from another block from `ChainID(X)`
//! we will split into a chain with a different ID:
//!
//! ```text
//! |
//! |
//! | split
//! |-------------
//! | | split
//! | |-------------|
//! | | |
//! | | |
//! | | |
//! \|/ \|/ \|/
//! main-chain ChainID(X) ChainID(Z)
//! ```
//!
//! As you can see if we wanted to get all the alt-blocks in `ChainID(Z)` that now includes some blocks from `ChainID(X)` as well.
//! [`get_alt_chain_history_ranges`] covers this and is the method to get the ranges of heights needed from each [`ChainID`](cuprate_types::ChainId)
//! to get all the alt-blocks in a given [`ChainID`](cuprate_types::ChainId).
//!
//! Although this should be kept in mind as a possibility, because Cuprate's block downloader will only track a single chain it is
//! unlikely that we will be tracking [`ChainID`](cuprate_types::ChainId)s that don't immediately connect to the main-chain.
//!
//! ## Why not use the block's `previous` field?
//!
//! Although that would be easier, it makes getting a range of block extremely slow, as we have to build the weight cache to verify
//! blocks, roughly 100,000 block headers needed, this cost is too high.
mod block;
mod chain;
mod tx;
pub use block::{
add_alt_block, flush_alt_blocks, get_alt_block, get_alt_block_extended_header_from_height,
get_alt_block_hash,
};
pub use chain::{get_alt_chain_history_ranges, update_alt_chain_info};
pub use tx::{add_alt_transaction_blob, get_alt_transaction};

View file

@ -0,0 +1,76 @@
use bytemuck::TransparentWrapper;
use monero_serai::transaction::Transaction;
use cuprate_database::{DatabaseRo, DatabaseRw, RuntimeError, StorableVec};
use cuprate_types::VerifiedTransactionInformation;
use crate::{
ops::macros::{doc_add_alt_block_inner_invariant, doc_error},
tables::{Tables, TablesMut},
types::{AltTransactionInfo, TxHash},
};
/// Adds a [`VerifiedTransactionInformation`] from an alt-block
/// if it is not already in the DB.
///
/// If the transaction is in the main-chain this function will still fill in the
/// [`AltTransactionInfos`](crate::tables::AltTransactionInfos) table, as that
/// table holds data which we don't keep around for main-chain txs.
///
#[doc = doc_add_alt_block_inner_invariant!()]
#[doc = doc_error!()]
pub fn add_alt_transaction_blob(
tx: &VerifiedTransactionInformation,
tables: &mut impl TablesMut,
) -> Result<(), RuntimeError> {
tables.alt_transaction_infos_mut().put(
&tx.tx_hash,
&AltTransactionInfo {
tx_weight: tx.tx_weight,
fee: tx.fee,
tx_hash: tx.tx_hash,
},
)?;
if tables.tx_ids().get(&tx.tx_hash).is_ok()
|| tables.alt_transaction_blobs().get(&tx.tx_hash).is_ok()
{
return Ok(());
}
tables
.alt_transaction_blobs_mut()
.put(&tx.tx_hash, StorableVec::wrap_ref(&tx.tx_blob))?;
Ok(())
}
/// Retrieve a [`VerifiedTransactionInformation`] from the database.
///
#[doc = doc_error!()]
pub fn get_alt_transaction(
tx_hash: &TxHash,
tables: &impl Tables,
) -> Result<VerifiedTransactionInformation, RuntimeError> {
let tx_info = tables.alt_transaction_infos().get(tx_hash)?;
let tx_blob = match tables.alt_transaction_blobs().get(tx_hash) {
Ok(blob) => blob.0,
Err(RuntimeError::KeyNotFound) => {
let tx_id = tables.tx_ids().get(tx_hash)?;
let blob = tables.tx_blobs().get(&tx_id)?;
blob.0
}
Err(e) => return Err(e),
};
Ok(VerifiedTransactionInformation {
tx: Transaction::read(&mut tx_blob.as_slice()).unwrap(),
tx_blob,
tx_weight: tx_info.tx_weight,
fee: tx_info.fee,
tx_hash: tx_info.tx_hash,
})
}

View file

@ -2,16 +2,26 @@
//---------------------------------------------------------------------------------------------------- Import
use bytemuck::TransparentWrapper;
use monero_serai::block::Block;
use monero_serai::{
block::{Block, BlockHeader},
transaction::Transaction,
};
use cuprate_database::{
RuntimeError, StorableVec, {DatabaseRo, DatabaseRw},
};
use cuprate_helper::map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits};
use cuprate_types::{ExtendedBlockHeader, HardFork, VerifiedBlockInformation};
use cuprate_helper::{
map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits},
tx::tx_fee,
};
use cuprate_types::{
AltBlockInformation, ChainId, ExtendedBlockHeader, HardFork, VerifiedBlockInformation,
VerifiedTransactionInformation,
};
use crate::{
ops::{
alt_block,
blockchain::{chain_height, cumulative_generated_coins},
macros::doc_error,
output::get_rct_num_outputs,
@ -33,11 +43,6 @@ use crate::{
/// This function will panic if:
/// - `block.height > u32::MAX` (not normally possible)
/// - `block.height` is not != [`chain_height`]
///
/// # Already exists
/// This function will operate normally even if `block` already
/// exists, i.e., this function will not return `Err` even if you
/// call this function infinitely with the same block.
// no inline, too big.
pub fn add_block(
block: &VerifiedBlockInformation,
@ -74,10 +79,10 @@ pub fn add_block(
//------------------------------------------------------ Transaction / Outputs / Key Images
// Add the miner transaction first.
{
let mining_tx_index = {
let tx = &block.block.miner_transaction;
add_tx(tx, &tx.serialize(), &tx.hash(), &chain_height, tables)?;
}
add_tx(tx, &tx.serialize(), &tx.hash(), &chain_height, tables)?
};
for tx in &block.txs {
add_tx(&tx.tx, &tx.tx_blob, &tx.tx_hash, &chain_height, tables)?;
@ -107,16 +112,23 @@ pub fn add_block(
cumulative_rct_outs,
timestamp: block.block.header.timestamp,
block_hash: block.block_hash,
// INVARIANT: #[cfg] @ lib.rs asserts `usize == u64`
weight: block.weight as u64,
long_term_weight: block.long_term_weight as u64,
weight: block.weight,
long_term_weight: block.long_term_weight,
mining_tx_index,
},
)?;
// Block blobs.
tables
.block_blobs_mut()
.put(&block.height, StorableVec::wrap_ref(&block.block_blob))?;
// Block header blob.
tables.block_header_blobs_mut().put(
&block.height,
StorableVec::wrap_ref(&block.block.header.serialize()),
)?;
// Block transaction hashes
tables.block_txs_hashes_mut().put(
&block.height,
StorableVec::wrap_ref(&block.block.transactions),
)?;
// Block heights.
tables
@ -130,37 +142,87 @@ pub fn add_block(
/// Remove the top/latest block from the database.
///
/// The removed block's data is returned.
///
/// If a [`ChainId`] is specified the popped block will be added to the alt block tables under
/// that [`ChainId`]. Otherwise, the block will be completely removed from the DB.
#[doc = doc_error!()]
///
/// In `pop_block()`'s case, [`RuntimeError::KeyNotFound`]
/// will be returned if there are no blocks left.
// no inline, too big
pub fn pop_block(
move_to_alt_chain: Option<ChainId>,
tables: &mut impl TablesMut,
) -> Result<(BlockHeight, BlockHash, Block), RuntimeError> {
//------------------------------------------------------ Block Info
// Remove block data from tables.
let (block_height, block_hash) = {
let (block_height, block_info) = tables.block_infos_mut().pop_last()?;
(block_height, block_info.block_hash)
};
let (block_height, block_info) = tables.block_infos_mut().pop_last()?;
// Block heights.
tables.block_heights_mut().delete(&block_hash)?;
tables.block_heights_mut().delete(&block_info.block_hash)?;
// Block blobs.
// We deserialize the block blob into a `Block`, such
// that we can remove the associated transactions later.
let block_blob = tables.block_blobs_mut().take(&block_height)?.0;
let block = Block::read(&mut block_blob.as_slice())?;
//
// We deserialize the block header blob and mining transaction blob
// to form a `Block`, such that we can remove the associated transactions
// later.
let block_header = tables.block_header_blobs_mut().take(&block_height)?.0;
let block_txs_hashes = tables.block_txs_hashes_mut().take(&block_height)?.0;
let miner_transaction = tables.tx_blobs().get(&block_info.mining_tx_index)?.0;
let block = Block {
header: BlockHeader::read(&mut block_header.as_slice())?,
miner_transaction: Transaction::read(&mut miner_transaction.as_slice())?,
transactions: block_txs_hashes,
};
//------------------------------------------------------ Transaction / Outputs / Key Images
remove_tx(&block.miner_transaction.hash(), tables)?;
for tx_hash in &block.transactions {
remove_tx(tx_hash, tables)?;
let remove_tx_iter = block.transactions.iter().map(|tx_hash| {
let (_, tx) = remove_tx(tx_hash, tables)?;
Ok::<_, RuntimeError>(tx)
});
if let Some(chain_id) = move_to_alt_chain {
let txs = remove_tx_iter
.map(|result| {
let tx = result?;
Ok(VerifiedTransactionInformation {
tx_weight: tx.weight(),
tx_blob: tx.serialize(),
tx_hash: tx.hash(),
fee: tx_fee(&tx),
tx,
})
})
.collect::<Result<Vec<VerifiedTransactionInformation>, RuntimeError>>()?;
alt_block::add_alt_block(
&AltBlockInformation {
block: block.clone(),
block_blob: block.serialize(),
txs,
block_hash: block_info.block_hash,
// We know the PoW is valid for this block so just set it so it will always verify as valid.
pow_hash: [0; 32],
height: block_height,
weight: block_info.weight,
long_term_weight: block_info.long_term_weight,
cumulative_difficulty: combine_low_high_bits_to_u128(
block_info.cumulative_difficulty_low,
block_info.cumulative_difficulty_high,
),
chain_id,
},
tables,
)?;
} else {
for result in remove_tx_iter {
drop(result?);
}
}
Ok((block_height, block_hash, block))
Ok((block_height, block_info.block_hash, block))
}
//---------------------------------------------------------------------------------------------------- `get_block_extended_header_*`
@ -193,26 +255,22 @@ pub fn get_block_extended_header_from_height(
tables: &impl Tables,
) -> Result<ExtendedBlockHeader, RuntimeError> {
let block_info = tables.block_infos().get(block_height)?;
let block_blob = tables.block_blobs().get(block_height)?.0;
let block = Block::read(&mut block_blob.as_slice())?;
let block_header_blob = tables.block_header_blobs().get(block_height)?.0;
let block_header = BlockHeader::read(&mut block_header_blob.as_slice())?;
let cumulative_difficulty = combine_low_high_bits_to_u128(
block_info.cumulative_difficulty_low,
block_info.cumulative_difficulty_high,
);
#[expect(
clippy::cast_possible_truncation,
reason = "INVARIANT: #[cfg] @ lib.rs asserts `usize == u64`"
)]
Ok(ExtendedBlockHeader {
cumulative_difficulty,
version: HardFork::from_version(block.header.hardfork_version)
version: HardFork::from_version(block_header.hardfork_version)
.expect("Stored block must have a valid hard-fork"),
vote: block.header.hardfork_signal,
timestamp: block.header.timestamp,
block_weight: block_info.weight as usize,
long_term_weight: block_info.long_term_weight as usize,
vote: block_header.hardfork_signal,
timestamp: block_header.timestamp,
block_weight: block_info.weight,
long_term_weight: block_info.long_term_weight,
})
}
@ -265,21 +323,21 @@ pub fn block_exists(
//---------------------------------------------------------------------------------------------------- Tests
#[cfg(test)]
#[expect(clippy::significant_drop_tightening, clippy::too_many_lines)]
#[expect(clippy::too_many_lines)]
mod test {
use pretty_assertions::assert_eq;
use cuprate_database::{Env, EnvInner, TxRw};
use cuprate_test_utils::data::{BLOCK_V16_TX0, BLOCK_V1_TX2, BLOCK_V9_TX3};
use super::*;
use crate::{
ops::tx::{get_tx, tx_exists},
tables::OpenTables,
tests::{assert_all_tables_are_empty, tmp_concrete_env, AssertTableLen},
};
use super::*;
/// Tests all above block functions.
///
/// Note that this doesn't test the correctness of values added, as the
@ -331,7 +389,8 @@ mod test {
// Assert only the proper tables were added to.
AssertTableLen {
block_infos: 3,
block_blobs: 3,
block_header_blobs: 3,
block_txs_hashes: 3,
block_heights: 3,
key_images: 69,
num_outputs: 41,
@ -414,7 +473,8 @@ mod test {
for block_hash in block_hashes.into_iter().rev() {
println!("pop_block(): block_hash: {}", hex::encode(block_hash));
let (_popped_height, popped_hash, _popped_block) = pop_block(&mut tables).unwrap();
let (_popped_height, popped_hash, _popped_block) =
pop_block(None, &mut tables).unwrap();
assert_eq!(block_hash, popped_hash);

View file

@ -138,7 +138,8 @@ mod test {
// Assert reads are correct.
AssertTableLen {
block_infos: 3,
block_blobs: 3,
block_header_blobs: 3,
block_txs_hashes: 3,
block_heights: 3,
key_images: 69,
num_outputs: 41,

View file

@ -31,3 +31,25 @@ When calling this function, ensure that either:
};
}
pub(super) use doc_add_block_inner_invariant;
/// Generate `# Invariant` documentation for internal alt block `fn`'s
/// that should be called directly with caution.
///
/// This is pretty much the same as [`doc_add_block_inner_invariant`],
/// it's not worth the effort to reduce the duplication.
macro_rules! doc_add_alt_block_inner_invariant {
() => {
r#"# ⚠️ Invariant ⚠️
This function mainly exists to be used internally by the parent function [`crate::ops::alt_block::add_alt_block`].
`add_alt_block()` makes sure all data related to the input is mutated, while
this function _does not_, it specifically mutates _particular_ tables.
This is usually undesired - although this function is still available to call directly.
When calling this function, ensure that either:
1. This effect (incomplete database mutation) is what is desired, or that...
2. ...the other tables will also be mutated to a correct state"#
};
}
pub(super) use doc_add_alt_block_inner_invariant;

View file

@ -94,7 +94,7 @@
//! // Read the data, assert it is correct.
//! let tx_rw = env_inner.tx_rw()?;
//! let mut tables = env_inner.open_tables_mut(&tx_rw)?;
//! let (height, hash, serai_block) = pop_block(&mut tables)?;
//! let (height, hash, serai_block) = pop_block(None, &mut tables)?;
//!
//! assert_eq!(height, 0);
//! assert_eq!(serai_block, block.block);
@ -102,6 +102,7 @@
//! # Ok(()) }
//! ```
pub mod alt_block;
pub mod block;
pub mod blockchain;
pub mod key_image;

View file

@ -316,7 +316,8 @@ mod test {
// Assert proper tables were added to.
AssertTableLen {
block_infos: 0,
block_blobs: 0,
block_header_blobs: 0,
block_txs_hashes: 0,
block_heights: 0,
key_images: 0,
num_outputs: 1,

View file

@ -366,7 +366,8 @@ mod test {
// Assert only the proper tables were added to.
AssertTableLen {
block_infos: 0,
block_blobs: 0,
block_header_blobs: 0,
block_txs_hashes: 0,
block_heights: 0,
key_images: 4, // added to key images
pruned_tx_blobs: 0,

View file

@ -4,11 +4,14 @@
use std::sync::Arc;
use cuprate_database::{ConcreteEnv, InitError};
use cuprate_types::{AltBlockInformation, VerifiedBlockInformation};
use crate::service::{init_read_service, init_write_service};
use crate::{
config::Config,
service::types::{BlockchainReadHandle, BlockchainWriteHandle},
service::{
init_read_service, init_write_service,
types::{BlockchainReadHandle, BlockchainWriteHandle},
},
};
//---------------------------------------------------------------------------------------------------- Init
@ -81,6 +84,44 @@ pub(super) const fn compact_history_genesis_not_included<const INITIAL_BLOCKS: u
top_block_height > INITIAL_BLOCKS && !(top_block_height - INITIAL_BLOCKS + 2).is_power_of_two()
}
//---------------------------------------------------------------------------------------------------- Map Block
/// Maps [`AltBlockInformation`] to [`VerifiedBlockInformation`]
///
/// # Panics
/// This will panic if the block is invalid, so should only be used on blocks that have been popped from
/// the main-chain.
pub(super) fn map_valid_alt_block_to_verified_block(
alt_block: AltBlockInformation,
) -> VerifiedBlockInformation {
let total_fees = alt_block.txs.iter().map(|tx| tx.fee).sum::<u64>();
let total_miner_output = alt_block
.block
.miner_transaction
.prefix()
.outputs
.iter()
.map(|out| out.amount.unwrap_or(0))
.sum::<u64>();
VerifiedBlockInformation {
block: alt_block.block,
block_blob: alt_block.block_blob,
txs: alt_block
.txs
.into_iter()
.map(TryInto::try_into)
.collect::<Result<_, _>>()
.unwrap(),
block_hash: alt_block.block_hash,
pow_hash: alt_block.pow_hash,
height: alt_block.height,
generated_coins: total_miner_output - total_fees,
weight: alt_block.weight,
long_term_weight: alt_block.long_term_weight,
cumulative_difficulty: alt_block.cumulative_difficulty,
}
}
//---------------------------------------------------------------------------------------------------- Tests
#[cfg(test)]

View file

@ -98,7 +98,7 @@
//!
//! // Block write was OK.
//! let response = response_channel.await?;
//! assert_eq!(response, BlockchainResponse::WriteBlockOk);
//! assert_eq!(response, BlockchainResponse::Ok);
//!
//! // Now, let's try getting the block hash
//! // of the block we just wrote.

View file

@ -8,6 +8,7 @@ use std::{
use rayon::{
iter::{IntoParallelIterator, ParallelIterator},
prelude::*,
ThreadPool,
};
use thread_local::ThreadLocal;
@ -17,11 +18,15 @@ use cuprate_database_service::{init_thread_pool, DatabaseReadService, ReaderThre
use cuprate_helper::map::combine_low_high_bits_to_u128;
use cuprate_types::{
blockchain::{BlockchainReadRequest, BlockchainResponse},
Chain, ExtendedBlockHeader, OutputOnChain,
Chain, ChainId, ExtendedBlockHeader, OutputOnChain,
};
use crate::{
ops::{
alt_block::{
get_alt_block, get_alt_block_extended_header_from_height, get_alt_block_hash,
get_alt_chain_history_ranges,
},
block::{
block_exists, get_block_extended_header_from_height, get_block_height, get_block_info,
},
@ -33,8 +38,10 @@ use crate::{
free::{compact_history_genesis_not_included, compact_history_index_to_height_offset},
types::{BlockchainReadHandle, ResponseResult},
},
tables::{BlockHeights, BlockInfos, OpenTables, Tables},
types::{Amount, AmountIndex, BlockHash, BlockHeight, KeyImage, PreRctOutputId},
tables::{AltBlockHeights, BlockHeights, BlockInfos, OpenTables, Tables},
types::{
AltBlockHeight, Amount, AmountIndex, BlockHash, BlockHeight, KeyImage, PreRctOutputId,
},
};
//---------------------------------------------------------------------------------------------------- init_read_service
@ -87,7 +94,7 @@ fn map_request(
match request {
R::BlockExtendedHeader(block) => block_extended_header(env, block),
R::BlockHash(block, chain) => block_hash(env, block, chain),
R::FindBlock(_) => todo!("Add alt blocks to DB"),
R::FindBlock(block_hash) => find_block(env, block_hash),
R::FilterUnknownHashes(hashes) => filter_unknown_hashes(env, hashes),
R::BlockExtendedHeaderInRange(range, chain) => {
block_extended_header_in_range(env, range, chain)
@ -99,6 +106,7 @@ fn map_request(
R::KeyImagesSpent(set) => key_images_spent(env, set),
R::CompactChainHistory => compact_chain_history(env),
R::FindFirstUnknown(block_ids) => find_first_unknown(env, &block_ids),
R::AltBlocksInChain(chain_id) => alt_blocks_in_chain(env, chain_id),
}
/* SOMEDAY: post-request handling, run some code for each request? */
@ -197,12 +205,41 @@ fn block_hash(env: &ConcreteEnv, block_height: BlockHeight, chain: Chain) -> Res
let block_hash = match chain {
Chain::Main => get_block_info(&block_height, &table_block_infos)?.block_hash,
Chain::Alt(_) => todo!("Add alt blocks to DB"),
Chain::Alt(chain) => {
get_alt_block_hash(&block_height, chain, &env_inner.open_tables(&tx_ro)?)?
}
};
Ok(BlockchainResponse::BlockHash(block_hash))
}
/// [`BlockchainReadRequest::FindBlock`]
fn find_block(env: &ConcreteEnv, block_hash: BlockHash) -> ResponseResult {
// Single-threaded, no `ThreadLocal` required.
let env_inner = env.env_inner();
let tx_ro = env_inner.tx_ro()?;
let table_block_heights = env_inner.open_db_ro::<BlockHeights>(&tx_ro)?;
// Check the main chain first.
match table_block_heights.get(&block_hash) {
Ok(height) => return Ok(BlockchainResponse::FindBlock(Some((Chain::Main, height)))),
Err(RuntimeError::KeyNotFound) => (),
Err(e) => return Err(e),
}
let table_alt_block_heights = env_inner.open_db_ro::<AltBlockHeights>(&tx_ro)?;
match table_alt_block_heights.get(&block_hash) {
Ok(height) => Ok(BlockchainResponse::FindBlock(Some((
Chain::Alt(height.chain_id.into()),
height.height,
)))),
Err(RuntimeError::KeyNotFound) => Ok(BlockchainResponse::FindBlock(None)),
Err(e) => Err(e),
}
}
/// [`BlockchainReadRequest::FilterUnknownHashes`].
#[inline]
fn filter_unknown_hashes(env: &ConcreteEnv, mut hashes: HashSet<BlockHash>) -> ResponseResult {
@ -253,7 +290,37 @@ fn block_extended_header_in_range(
get_block_extended_header_from_height(&block_height, tables)
})
.collect::<Result<Vec<ExtendedBlockHeader>, RuntimeError>>()?,
Chain::Alt(_) => todo!("Add alt blocks to DB"),
Chain::Alt(chain_id) => {
let ranges = {
let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?;
let tables = get_tables!(env_inner, tx_ro, tables)?.as_ref();
let alt_chains = tables.alt_chain_infos();
get_alt_chain_history_ranges(range, chain_id, alt_chains)?
};
ranges
.par_iter()
.rev()
.flat_map(|(chain, range)| {
range.clone().into_par_iter().map(|height| {
let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?;
let tables = get_tables!(env_inner, tx_ro, tables)?.as_ref();
match *chain {
Chain::Main => get_block_extended_header_from_height(&height, tables),
Chain::Alt(chain_id) => get_alt_block_extended_header_from_height(
&AltBlockHeight {
chain_id: chain_id.into(),
height,
},
tables,
),
}
})
})
.collect::<Result<Vec<_>, _>>()?
}
};
Ok(BlockchainResponse::BlockExtendedHeaderInRange(vec))
@ -492,3 +559,45 @@ fn find_first_unknown(env: &ConcreteEnv, block_ids: &[BlockHash]) -> ResponseRes
BlockchainResponse::FindFirstUnknown(Some((idx, last_known_height + 1)))
})
}
/// [`BlockchainReadRequest::AltBlocksInChain`]
fn alt_blocks_in_chain(env: &ConcreteEnv, chain_id: ChainId) -> ResponseResult {
// Prepare tx/tables in `ThreadLocal`.
let env_inner = env.env_inner();
let tx_ro = thread_local(env);
let tables = thread_local(env);
// Get the history of this alt-chain.
let history = {
let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?;
let tables = get_tables!(env_inner, tx_ro, tables)?.as_ref();
get_alt_chain_history_ranges(0..usize::MAX, chain_id, tables.alt_chain_infos())?
};
// Get all the blocks until we join the main-chain.
let blocks = history
.par_iter()
.rev()
.skip(1)
.flat_map(|(chain_id, range)| {
let Chain::Alt(chain_id) = chain_id else {
panic!("Should not have main chain blocks here we skipped last range");
};
range.clone().into_par_iter().map(|height| {
let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?;
let tables = get_tables!(env_inner, tx_ro, tables)?.as_ref();
get_alt_block(
&AltBlockHeight {
chain_id: (*chain_id).into(),
height,
},
tables,
)
})
})
.collect::<Result<_, _>>()?;
Ok(BlockchainResponse::AltBlocksInChain(blocks))
}

View file

@ -13,13 +13,14 @@ use std::{
};
use pretty_assertions::assert_eq;
use rand::Rng;
use tower::{Service, ServiceExt};
use cuprate_database::{ConcreteEnv, DatabaseIter, DatabaseRo, Env, EnvInner, RuntimeError};
use cuprate_test_utils::data::{BLOCK_V16_TX0, BLOCK_V1_TX2, BLOCK_V9_TX3};
use cuprate_types::{
blockchain::{BlockchainReadRequest, BlockchainResponse, BlockchainWriteRequest},
Chain, OutputOnChain, VerifiedBlockInformation,
Chain, ChainId, OutputOnChain, VerifiedBlockInformation,
};
use crate::{
@ -31,7 +32,7 @@ use crate::{
},
service::{init, BlockchainReadHandle, BlockchainWriteHandle},
tables::{OpenTables, Tables, TablesIter},
tests::AssertTableLen,
tests::{map_verified_block_to_alt, AssertTableLen},
types::{Amount, AmountIndex, PreRctOutputId},
};
@ -87,7 +88,7 @@ async fn test_template(
let request = BlockchainWriteRequest::WriteBlock(block);
let response_channel = writer.call(request);
let response = response_channel.await.unwrap();
assert_eq!(response, BlockchainResponse::WriteBlockOk);
assert_eq!(response, BlockchainResponse::Ok);
}
//----------------------------------------------------------------------- Reset the transaction
@ -240,42 +241,38 @@ async fn test_template(
//----------------------------------------------------------------------- Output checks
// Create the map of amounts and amount indices.
//
// FIXME: There's definitely a better way to map
// `Vec<PreRctOutputId>` -> `HashMap<u64, HashSet<u64>>`
let (map, output_count) = {
let mut ids = tables
.outputs_iter()
.keys()
.unwrap()
.map(Result::unwrap)
.collect::<Vec<PreRctOutputId>>();
ids.extend(
tables
.rct_outputs_iter()
.keys()
.unwrap()
.map(Result::unwrap)
.map(|amount_index| PreRctOutputId {
amount: 0,
amount_index,
}),
);
let mut map = HashMap::<Amount, HashSet<AmountIndex>>::new();
// Used later to compare the amount of Outputs
// returned in the Response is equal to the amount
// we asked for.
let output_count = ids.len();
let mut output_count: usize = 0;
let mut map = HashMap::<Amount, HashSet<AmountIndex>>::new();
for id in ids {
map.entry(id.amount)
.and_modify(|set| {
set.insert(id.amount_index);
})
.or_insert_with(|| HashSet::from([id.amount_index]));
}
tables
.outputs_iter()
.keys()
.unwrap()
.map(Result::unwrap)
.chain(
tables
.rct_outputs_iter()
.keys()
.unwrap()
.map(Result::unwrap)
.map(|amount_index| PreRctOutputId {
amount: 0,
amount_index,
}),
)
.for_each(|id| {
output_count += 1;
map.entry(id.amount)
.and_modify(|set| {
set.insert(id.amount_index);
})
.or_insert_with(|| HashSet::from([id.amount_index]));
});
(map, output_count)
};
@ -346,7 +343,8 @@ async fn v1_tx2() {
14_535_350_982_449,
AssertTableLen {
block_infos: 1,
block_blobs: 1,
block_header_blobs: 1,
block_txs_hashes: 1,
block_heights: 1,
key_images: 65,
num_outputs: 41,
@ -372,7 +370,8 @@ async fn v9_tx3() {
3_403_774_022_163,
AssertTableLen {
block_infos: 1,
block_blobs: 1,
block_header_blobs: 1,
block_txs_hashes: 1,
block_heights: 1,
key_images: 4,
num_outputs: 0,
@ -398,7 +397,8 @@ async fn v16_tx0() {
600_000_000_000,
AssertTableLen {
block_infos: 1,
block_blobs: 1,
block_header_blobs: 1,
block_txs_hashes: 1,
block_heights: 1,
key_images: 0,
num_outputs: 0,
@ -415,3 +415,92 @@ async fn v16_tx0() {
)
.await;
}
/// Tests the alt-chain requests and responses.
#[tokio::test]
async fn alt_chain_requests() {
let (reader, mut writer, _, _tempdir) = init_service();
// Set up the test by adding blocks to the main-chain.
for (i, mut block) in [BLOCK_V9_TX3.clone(), BLOCK_V16_TX0.clone()]
.into_iter()
.enumerate()
{
block.height = i;
let request = BlockchainWriteRequest::WriteBlock(block);
writer.call(request).await.unwrap();
}
// Generate the alt-blocks.
let mut prev_hash = BLOCK_V9_TX3.block_hash;
let mut chain_id = 1;
let alt_blocks = [&BLOCK_V16_TX0, &BLOCK_V9_TX3, &BLOCK_V1_TX2]
.into_iter()
.enumerate()
.map(|(i, block)| {
let mut block = (**block).clone();
block.height = i + 1;
block.block.header.previous = prev_hash;
block.block_blob = block.block.serialize();
prev_hash = block.block_hash;
// Randomly either keep the [`ChainId`] the same or change it to a new value.
chain_id += rand::thread_rng().gen_range(0..=1);
map_verified_block_to_alt(block, ChainId(chain_id.try_into().unwrap()))
})
.collect::<Vec<_>>();
for block in &alt_blocks {
// Request a block to be written, assert it was written.
let request = BlockchainWriteRequest::WriteAltBlock(block.clone());
let response_channel = writer.call(request);
let response = response_channel.await.unwrap();
assert_eq!(response, BlockchainResponse::Ok);
}
// Get the full alt-chain
let request = BlockchainReadRequest::AltBlocksInChain(ChainId(chain_id.try_into().unwrap()));
let response = reader.clone().oneshot(request).await.unwrap();
let BlockchainResponse::AltBlocksInChain(blocks) = response else {
panic!("Wrong response type was returned");
};
assert_eq!(blocks.len(), alt_blocks.len());
for (got_block, alt_block) in blocks.into_iter().zip(alt_blocks) {
assert_eq!(got_block.block_blob, alt_block.block_blob);
assert_eq!(got_block.block_hash, alt_block.block_hash);
assert_eq!(got_block.chain_id, alt_block.chain_id);
assert_eq!(got_block.txs, alt_block.txs);
}
// Flush all alt blocks.
let request = BlockchainWriteRequest::FlushAltBlocks;
let response = writer.ready().await.unwrap().call(request).await.unwrap();
assert_eq!(response, BlockchainResponse::Ok);
// Pop blocks from the main chain
let request = BlockchainWriteRequest::PopBlocks(1);
let response = writer.ready().await.unwrap().call(request).await.unwrap();
let BlockchainResponse::PopBlocks(old_main_chain_id) = response else {
panic!("Wrong response type was returned");
};
// Check we have popped the top block.
let request = BlockchainReadRequest::ChainHeight;
let response = reader.clone().oneshot(request).await.unwrap();
assert!(matches!(response, BlockchainResponse::ChainHeight(1, _)));
// Attempt to add the popped block back.
let request = BlockchainWriteRequest::ReverseReorg(old_main_chain_id);
let response = writer.ready().await.unwrap().call(request).await.unwrap();
assert_eq!(response, BlockchainResponse::Ok);
// Check we have the popped block back.
let request = BlockchainReadRequest::ChainHeight;
let response = reader.clone().oneshot(request).await.unwrap();
assert!(matches!(response, BlockchainResponse::ChainHeight(2, _)));
}

View file

@ -1,20 +1,30 @@
//! Database writer thread definitions and logic.
//---------------------------------------------------------------------------------------------------- Import
use std::sync::Arc;
use cuprate_database::{ConcreteEnv, Env, EnvInner, RuntimeError, TxRw};
use cuprate_database::{ConcreteEnv, DatabaseRo, Env, EnvInner, RuntimeError, TxRw};
use cuprate_database_service::DatabaseWriteHandle;
use cuprate_types::{
blockchain::{BlockchainResponse, BlockchainWriteRequest},
VerifiedBlockInformation,
AltBlockInformation, Chain, ChainId, VerifiedBlockInformation,
};
use crate::{
service::types::{BlockchainWriteHandle, ResponseResult},
tables::OpenTables,
service::{
free::map_valid_alt_block_to_verified_block,
types::{BlockchainWriteHandle, ResponseResult},
},
tables::{OpenTables, Tables},
types::AltBlockHeight,
};
/// Write functions within this module abort if the write transaction
/// could not be aborted successfully to maintain atomicity.
///
/// This is the panic message if the `abort()` fails.
const TX_RW_ABORT_FAIL: &str =
"Could not maintain blockchain database atomicity by aborting write transaction";
//---------------------------------------------------------------------------------------------------- init_write_service
/// Initialize the blockchain write service from a [`ConcreteEnv`].
pub fn init_write_service(env: Arc<ConcreteEnv>) -> BlockchainWriteHandle {
@ -29,6 +39,12 @@ fn handle_blockchain_request(
) -> Result<BlockchainResponse, RuntimeError> {
match req {
BlockchainWriteRequest::WriteBlock(block) => write_block(env, block),
BlockchainWriteRequest::WriteAltBlock(alt_block) => write_alt_block(env, alt_block),
BlockchainWriteRequest::PopBlocks(numb_blocks) => pop_blocks(env, *numb_blocks),
BlockchainWriteRequest::ReverseReorg(old_main_chain_id) => {
reverse_reorg(env, *old_main_chain_id)
}
BlockchainWriteRequest::FlushAltBlocks => flush_alt_blocks(env),
}
}
@ -55,13 +71,140 @@ fn write_block(env: &ConcreteEnv, block: &VerifiedBlockInformation) -> ResponseR
match result {
Ok(()) => {
TxRw::commit(tx_rw)?;
Ok(BlockchainResponse::WriteBlockOk)
Ok(BlockchainResponse::Ok)
}
Err(e) => {
// INVARIANT: ensure database atomicity by aborting
// the transaction on `add_block()` failures.
TxRw::abort(tx_rw)
.expect("could not maintain database atomicity by aborting write transaction");
TxRw::abort(tx_rw).expect(TX_RW_ABORT_FAIL);
Err(e)
}
}
}
/// [`BlockchainWriteRequest::WriteAltBlock`].
#[inline]
fn write_alt_block(env: &ConcreteEnv, block: &AltBlockInformation) -> ResponseResult {
let env_inner = env.env_inner();
let tx_rw = env_inner.tx_rw()?;
let result = {
let mut tables_mut = env_inner.open_tables_mut(&tx_rw)?;
crate::ops::alt_block::add_alt_block(block, &mut tables_mut)
};
match result {
Ok(()) => {
TxRw::commit(tx_rw)?;
Ok(BlockchainResponse::Ok)
}
Err(e) => {
TxRw::abort(tx_rw).expect(TX_RW_ABORT_FAIL);
Err(e)
}
}
}
/// [`BlockchainWriteRequest::PopBlocks`].
fn pop_blocks(env: &ConcreteEnv, numb_blocks: usize) -> ResponseResult {
let env_inner = env.env_inner();
let mut tx_rw = env_inner.tx_rw()?;
// FIXME: turn this function into a try block once stable.
let mut result = || {
// flush all the current alt blocks as they may reference blocks to be popped.
crate::ops::alt_block::flush_alt_blocks(&env_inner, &mut tx_rw)?;
let mut tables_mut = env_inner.open_tables_mut(&tx_rw)?;
// generate a `ChainId` for the popped blocks.
let old_main_chain_id = ChainId(rand::random());
// pop the blocks
for _ in 0..numb_blocks {
crate::ops::block::pop_block(Some(old_main_chain_id), &mut tables_mut)?;
}
Ok(old_main_chain_id)
};
match result() {
Ok(old_main_chain_id) => {
TxRw::commit(tx_rw)?;
Ok(BlockchainResponse::PopBlocks(old_main_chain_id))
}
Err(e) => {
TxRw::abort(tx_rw).expect(TX_RW_ABORT_FAIL);
Err(e)
}
}
}
/// [`BlockchainWriteRequest::ReverseReorg`].
fn reverse_reorg(env: &ConcreteEnv, chain_id: ChainId) -> ResponseResult {
let env_inner = env.env_inner();
let mut tx_rw = env_inner.tx_rw()?;
// FIXME: turn this function into a try block once stable.
let mut result = || {
let mut tables_mut = env_inner.open_tables_mut(&tx_rw)?;
let chain_info = tables_mut.alt_chain_infos().get(&chain_id.into())?;
// Although this doesn't guarantee the chain was popped from the main-chain, it's an easy
// thing for us to check.
assert_eq!(Chain::from(chain_info.parent_chain), Chain::Main);
let top_block_height =
crate::ops::blockchain::top_block_height(tables_mut.block_heights())?;
// pop any blocks that were added as part of a re-org.
for _ in chain_info.common_ancestor_height..top_block_height {
crate::ops::block::pop_block(None, &mut tables_mut)?;
}
// Add the old main chain blocks back to the main chain.
for height in (chain_info.common_ancestor_height + 1)..chain_info.chain_height {
let alt_block = crate::ops::alt_block::get_alt_block(
&AltBlockHeight {
chain_id: chain_id.into(),
height,
},
&tables_mut,
)?;
let verified_block = map_valid_alt_block_to_verified_block(alt_block);
crate::ops::block::add_block(&verified_block, &mut tables_mut)?;
}
drop(tables_mut);
crate::ops::alt_block::flush_alt_blocks(&env_inner, &mut tx_rw)?;
Ok(())
};
match result() {
Ok(()) => {
TxRw::commit(tx_rw)?;
Ok(BlockchainResponse::Ok)
}
Err(e) => {
TxRw::abort(tx_rw).expect(TX_RW_ABORT_FAIL);
Err(e)
}
}
}
/// [`BlockchainWriteRequest::FlushAltBlocks`].
#[inline]
fn flush_alt_blocks(env: &ConcreteEnv) -> ResponseResult {
let env_inner = env.env_inner();
let mut tx_rw = env_inner.tx_rw()?;
let result = crate::ops::alt_block::flush_alt_blocks(&env_inner, &mut tx_rw);
match result {
Ok(()) => {
TxRw::commit(tx_rw)?;
Ok(BlockchainResponse::Ok)
}
Err(e) => {
TxRw::abort(tx_rw).expect(TX_RW_ABORT_FAIL);
Err(e)
}
}

View file

@ -9,7 +9,7 @@
//! Table structs are `CamelCase`, and their static string
//! names used by the actual database backend are `snake_case`.
//!
//! For example: [`BlockBlobs`] -> `block_blobs`.
//! For example: [`BlockHeaderBlobs`] -> `block_header_blobs`.
//!
//! # Traits
//! This module also contains a set of traits for
@ -17,9 +17,10 @@
//---------------------------------------------------------------------------------------------------- Import
use crate::types::{
Amount, AmountIndex, AmountIndices, BlockBlob, BlockHash, BlockHeight, BlockInfo, KeyImage,
Output, PreRctOutputId, PrunableBlob, PrunableHash, PrunedBlob, RctOutput, TxBlob, TxHash,
TxId, UnlockTime,
AltBlockHeight, AltChainInfo, AltTransactionInfo, Amount, AmountIndex, AmountIndices,
BlockBlob, BlockHash, BlockHeaderBlob, BlockHeight, BlockInfo, BlockTxHashes,
CompactAltBlockInfo, KeyImage, Output, PreRctOutputId, PrunableBlob, PrunableHash, PrunedBlob,
RawChainId, RctOutput, TxBlob, TxHash, TxId, UnlockTime,
};
//---------------------------------------------------------------------------------------------------- Tables
@ -29,22 +30,28 @@ use crate::types::{
// - If adding/changing a table also edit:
// - the tests in `src/backend/tests.rs`
cuprate_database::define_tables! {
/// Serialized block blobs (bytes).
/// Serialized block header blobs (bytes).
///
/// Contains the serialized version of all blocks.
0 => BlockBlobs,
BlockHeight => BlockBlob,
/// Contains the serialized version of all blocks headers.
0 => BlockHeaderBlobs,
BlockHeight => BlockHeaderBlob,
/// Block transactions hashes
///
/// Contains all the transaction hashes of all blocks.
1 => BlockTxsHashes,
BlockHeight => BlockTxHashes,
/// Block heights.
///
/// Contains the height of all blocks.
1 => BlockHeights,
2 => BlockHeights,
BlockHash => BlockHeight,
/// Block information.
///
/// Contains metadata of all blocks.
2 => BlockInfos,
3 => BlockInfos,
BlockHeight => BlockInfo,
/// Set of key images.
@ -53,38 +60,38 @@ cuprate_database::define_tables! {
///
/// This table has `()` as the value type, as in,
/// it is a set of key images.
3 => KeyImages,
4 => KeyImages,
KeyImage => (),
/// Maps an output's amount to the number of outputs with that amount.
///
/// For example, if there are 5 outputs with `amount = 123`
/// then calling `get(123)` on this table will return 5.
4 => NumOutputs,
5 => NumOutputs,
Amount => u64,
/// Pre-RCT output data.
5 => Outputs,
6 => Outputs,
PreRctOutputId => Output,
/// Pruned transaction blobs (bytes).
///
/// Contains the pruned portion of serialized transaction data.
6 => PrunedTxBlobs,
7 => PrunedTxBlobs,
TxId => PrunedBlob,
/// Prunable transaction blobs (bytes).
///
/// Contains the prunable portion of serialized transaction data.
// SOMEDAY: impl when `monero-serai` supports pruning
7 => PrunableTxBlobs,
8 => PrunableTxBlobs,
TxId => PrunableBlob,
/// Prunable transaction hashes.
///
/// Contains the prunable portion of transaction hashes.
// SOMEDAY: impl when `monero-serai` supports pruning
8 => PrunableHashes,
9 => PrunableHashes,
TxId => PrunableHash,
// SOMEDAY: impl a properties table:
@ -94,41 +101,75 @@ cuprate_database::define_tables! {
// StorableString => StorableVec,
/// RCT output data.
9 => RctOutputs,
10 => RctOutputs,
AmountIndex => RctOutput,
/// Transaction blobs (bytes).
///
/// Contains the serialized version of all transactions.
// SOMEDAY: remove when `monero-serai` supports pruning
10 => TxBlobs,
11 => TxBlobs,
TxId => TxBlob,
/// Transaction indices.
///
/// Contains the indices all transactions.
11 => TxIds,
12 => TxIds,
TxHash => TxId,
/// Transaction heights.
///
/// Contains the block height associated with all transactions.
12 => TxHeights,
13 => TxHeights,
TxId => BlockHeight,
/// Transaction outputs.
///
/// Contains the list of `AmountIndex`'s of the
/// outputs associated with all transactions.
13 => TxOutputs,
14 => TxOutputs,
TxId => AmountIndices,
/// Transaction unlock time.
///
/// Contains the unlock time of transactions IF they have one.
/// Transactions without unlock times will not exist in this table.
14 => TxUnlockTime,
15 => TxUnlockTime,
TxId => UnlockTime,
/// Information on alt-chains.
16 => AltChainInfos,
RawChainId => AltChainInfo,
/// Alt-block heights.
///
/// Contains the height of all alt-blocks.
17 => AltBlockHeights,
BlockHash => AltBlockHeight,
/// Alt-block information.
///
/// Contains information on all alt-blocks.
18 => AltBlocksInfo,
AltBlockHeight => CompactAltBlockInfo,
/// Alt-block blobs.
///
/// Contains the raw bytes of all alt-blocks.
19 => AltBlockBlobs,
AltBlockHeight => BlockBlob,
/// Alt-block transaction blobs.
///
/// Contains the raw bytes of alt transactions, if those transactions are not in the main-chain.
20 => AltTransactionBlobs,
TxHash => TxBlob,
/// Alt-block transaction information.
///
/// Contains information on all alt transactions, even if they are in the main-chain.
21 => AltTransactionInfos,
TxHash => AltTransactionInfo,
}
//---------------------------------------------------------------------------------------------------- Tests

View file

@ -9,7 +9,8 @@ use std::{borrow::Cow, fmt::Debug};
use pretty_assertions::assert_eq;
use cuprate_database::{ConcreteEnv, DatabaseRo, Env, EnvInner};
use cuprate_database::{DatabaseRo, Env, EnvInner};
use cuprate_types::{AltBlockInformation, ChainId, VerifiedBlockInformation};
use crate::{
config::ConfigBuilder,
@ -25,7 +26,8 @@ use crate::{
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct AssertTableLen {
pub(crate) block_infos: u64,
pub(crate) block_blobs: u64,
pub(crate) block_header_blobs: u64,
pub(crate) block_txs_hashes: u64,
pub(crate) block_heights: u64,
pub(crate) key_images: u64,
pub(crate) num_outputs: u64,
@ -45,7 +47,8 @@ impl AssertTableLen {
pub(crate) fn assert(self, tables: &impl Tables) {
let other = Self {
block_infos: tables.block_infos().len().unwrap(),
block_blobs: tables.block_blobs().len().unwrap(),
block_header_blobs: tables.block_header_blobs().len().unwrap(),
block_txs_hashes: tables.block_txs_hashes().len().unwrap(),
block_heights: tables.block_heights().len().unwrap(),
key_images: tables.key_images().len().unwrap(),
num_outputs: tables.num_outputs().len().unwrap(),
@ -68,8 +71,7 @@ impl AssertTableLen {
/// Create an `Env` in a temporarily directory.
/// The directory is automatically removed after the `TempDir` is dropped.
///
/// FIXME: changing this to `-> impl Env` causes lifetime errors...
pub(crate) fn tmp_concrete_env() -> (ConcreteEnv, tempfile::TempDir) {
pub(crate) fn tmp_concrete_env() -> (impl Env, tempfile::TempDir) {
let tempdir = tempfile::tempdir().unwrap();
let config = ConfigBuilder::new()
.db_directory(Cow::Owned(tempdir.path().into()))
@ -81,10 +83,28 @@ pub(crate) fn tmp_concrete_env() -> (ConcreteEnv, tempfile::TempDir) {
}
/// Assert all the tables in the environment are empty.
pub(crate) fn assert_all_tables_are_empty(env: &ConcreteEnv) {
pub(crate) fn assert_all_tables_are_empty(env: &impl Env) {
let env_inner = env.env_inner();
let tx_ro = env_inner.tx_ro().unwrap();
let tables = env_inner.open_tables(&tx_ro).unwrap();
assert!(tables.all_tables_empty().unwrap());
assert_eq!(crate::ops::tx::get_num_tx(tables.tx_ids()).unwrap(), 0);
}
pub(crate) fn map_verified_block_to_alt(
verified_block: VerifiedBlockInformation,
chain_id: ChainId,
) -> AltBlockInformation {
AltBlockInformation {
block: verified_block.block,
block_blob: verified_block.block_blob,
txs: verified_block.txs,
block_hash: verified_block.block_hash,
pow_hash: verified_block.pow_hash,
height: verified_block.height,
weight: verified_block.weight,
long_term_weight: verified_block.long_term_weight,
cumulative_difficulty: verified_block.cumulative_difficulty,
chain_id,
}
}

View file

@ -41,12 +41,14 @@
#![forbid(unsafe_code)] // if you remove this line i will steal your monero
//---------------------------------------------------------------------------------------------------- Import
use bytemuck::{Pod, Zeroable};
use std::num::NonZero;
use bytemuck::{Pod, Zeroable};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use cuprate_database::{Key, StorableVec};
use cuprate_types::{Chain, ChainId};
//---------------------------------------------------------------------------------------------------- Aliases
// These type aliases exist as many Monero-related types are the exact same.
@ -64,6 +66,12 @@ pub type AmountIndices = StorableVec<AmountIndex>;
/// A serialized block.
pub type BlockBlob = StorableVec<u8>;
/// A serialized block header
pub type BlockHeaderBlob = StorableVec<u8>;
/// A block transaction hashes
pub type BlockTxHashes = StorableVec<[u8; 32]>;
/// A block's hash.
pub type BlockHash = [u8; 32];
@ -164,6 +172,7 @@ impl Key for PreRctOutputId {}
/// block_hash: [54; 32],
/// cumulative_rct_outs: 2389,
/// long_term_weight: 2389,
/// mining_tx_index: 23
/// };
/// let b = Storable::as_bytes(&a);
/// let c: BlockInfo = Storable::from_bytes(b);
@ -173,7 +182,7 @@ impl Key for PreRctOutputId {}
/// # Size & Alignment
/// ```rust
/// # use cuprate_blockchain::types::*;
/// assert_eq!(size_of::<BlockInfo>(), 88);
/// assert_eq!(size_of::<BlockInfo>(), 96);
/// assert_eq!(align_of::<BlockInfo>(), 8);
/// ```
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@ -187,7 +196,7 @@ pub struct BlockInfo {
/// The adjusted block size, in bytes.
///
/// See [`block_weight`](https://monero-book.cuprate.org/consensus_rules/blocks/weights.html#blocks-weight).
pub weight: u64,
pub weight: usize,
/// Least-significant 64 bits of the 128-bit cumulative difficulty.
pub cumulative_difficulty_low: u64,
/// Most-significant 64 bits of the 128-bit cumulative difficulty.
@ -199,7 +208,9 @@ pub struct BlockInfo {
/// The long term block weight, based on the median weight of the preceding `100_000` blocks.
///
/// See [`long_term_weight`](https://monero-book.cuprate.org/consensus_rules/blocks/weights.html#long-term-block-weight).
pub long_term_weight: u64,
pub long_term_weight: usize,
/// [`TxId`] (u64) of the block coinbase transaction.
pub mining_tx_index: TxId,
}
//---------------------------------------------------------------------------------------------------- OutputFlags
@ -324,6 +335,259 @@ pub struct RctOutput {
}
// TODO: local_index?
//---------------------------------------------------------------------------------------------------- RawChain
/// [`Chain`] in a format which can be stored in the DB.
///
/// Implements [`Into`] and [`From`] for [`Chain`].
///
/// ```rust
/// # use std::borrow::*;
/// # use cuprate_blockchain::{*, types::*};
/// use cuprate_database::Storable;
/// use cuprate_types::Chain;
///
/// // Assert Storable is correct.
/// let a: RawChain = Chain::Main.into();
/// let b = Storable::as_bytes(&a);
/// let c: RawChain = Storable::from_bytes(b);
/// assert_eq!(a, c);
/// ```
///
/// # Size & Alignment
/// ```rust
/// # use cuprate_blockchain::types::*;
/// assert_eq!(size_of::<RawChain>(), 8);
/// assert_eq!(align_of::<RawChain>(), 8);
/// ```
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)]
#[repr(transparent)]
pub struct RawChain(u64);
impl From<Chain> for RawChain {
fn from(value: Chain) -> Self {
match value {
Chain::Main => Self(0),
Chain::Alt(chain_id) => Self(chain_id.0.get()),
}
}
}
impl From<RawChain> for Chain {
fn from(value: RawChain) -> Self {
NonZero::new(value.0).map_or(Self::Main, |id| Self::Alt(ChainId(id)))
}
}
impl From<RawChainId> for RawChain {
fn from(value: RawChainId) -> Self {
// A [`ChainID`] with an inner value of `0` is invalid.
assert_ne!(value.0, 0);
Self(value.0)
}
}
//---------------------------------------------------------------------------------------------------- RawChainId
/// [`ChainId`] in a format which can be stored in the DB.
///
/// Implements [`Into`] and [`From`] for [`ChainId`].
///
/// ```rust
/// # use std::borrow::*;
/// # use cuprate_blockchain::{*, types::*};
/// use cuprate_database::Storable;
/// use cuprate_types::ChainId;
///
/// // Assert Storable is correct.
/// let a: RawChainId = ChainId(10.try_into().unwrap()).into();
/// let b = Storable::as_bytes(&a);
/// let c: RawChainId = Storable::from_bytes(b);
/// assert_eq!(a, c);
/// ```
///
/// # Size & Alignment
/// ```rust
/// # use cuprate_blockchain::types::*;
/// assert_eq!(size_of::<RawChainId>(), 8);
/// assert_eq!(align_of::<RawChainId>(), 8);
/// ```
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)]
#[repr(transparent)]
pub struct RawChainId(u64);
impl From<ChainId> for RawChainId {
fn from(value: ChainId) -> Self {
Self(value.0.get())
}
}
impl From<RawChainId> for ChainId {
fn from(value: RawChainId) -> Self {
Self(NonZero::new(value.0).expect("RawChainId cannot have a value of `0`"))
}
}
impl Key for RawChainId {}
//---------------------------------------------------------------------------------------------------- AltChainInfo
/// Information on an alternative chain.
///
/// ```rust
/// # use std::borrow::*;
/// # use cuprate_blockchain::{*, types::*};
/// use cuprate_database::Storable;
/// use cuprate_types::Chain;
///
/// // Assert Storable is correct.
/// let a: AltChainInfo = AltChainInfo {
/// parent_chain: Chain::Main.into(),
/// common_ancestor_height: 0,
/// chain_height: 1,
/// };
/// let b = Storable::as_bytes(&a);
/// let c: AltChainInfo = Storable::from_bytes(b);
/// assert_eq!(a, c);
/// ```
///
/// # Size & Alignment
/// ```rust
/// # use cuprate_blockchain::types::*;
/// assert_eq!(size_of::<AltChainInfo>(), 24);
/// assert_eq!(align_of::<AltChainInfo>(), 8);
/// ```
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)]
#[repr(C)]
pub struct AltChainInfo {
/// The chain this alt chain forks from.
pub parent_chain: RawChain,
/// The height of the first block we share with the parent chain.
pub common_ancestor_height: usize,
/// The chain height of the blocks in this alt chain.
pub chain_height: usize,
}
//---------------------------------------------------------------------------------------------------- AltBlockHeight
/// Represents the height of a block on an alt-chain.
///
/// ```rust
/// # use std::borrow::*;
/// # use cuprate_blockchain::{*, types::*};
/// use cuprate_database::Storable;
/// use cuprate_types::ChainId;
///
/// // Assert Storable is correct.
/// let a: AltBlockHeight = AltBlockHeight {
/// chain_id: ChainId(1.try_into().unwrap()).into(),
/// height: 1,
/// };
/// let b = Storable::as_bytes(&a);
/// let c: AltBlockHeight = Storable::from_bytes(b);
/// assert_eq!(a, c);
/// ```
///
/// # Size & Alignment
/// ```rust
/// # use cuprate_blockchain::types::*;
/// assert_eq!(size_of::<AltBlockHeight>(), 16);
/// assert_eq!(align_of::<AltBlockHeight>(), 8);
/// ```
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)]
#[repr(C)]
pub struct AltBlockHeight {
/// The [`ChainId`] of the chain this alt block is on, in raw form.
pub chain_id: RawChainId,
/// The height of this alt-block.
pub height: usize,
}
impl Key for AltBlockHeight {}
//---------------------------------------------------------------------------------------------------- CompactAltBlockInfo
/// Represents information on an alt-chain.
///
/// ```rust
/// # use std::borrow::*;
/// # use cuprate_blockchain::{*, types::*};
/// use cuprate_database::Storable;
///
/// // Assert Storable is correct.
/// let a: CompactAltBlockInfo = CompactAltBlockInfo {
/// block_hash: [1; 32],
/// pow_hash: [2; 32],
/// height: 10,
/// weight: 20,
/// long_term_weight: 30,
/// cumulative_difficulty_low: 40,
/// cumulative_difficulty_high: 50,
/// };
///
/// let b = Storable::as_bytes(&a);
/// let c: CompactAltBlockInfo = Storable::from_bytes(b);
/// assert_eq!(a, c);
/// ```
///
/// # Size & Alignment
/// ```rust
/// # use cuprate_blockchain::types::*;
/// assert_eq!(size_of::<CompactAltBlockInfo>(), 104);
/// assert_eq!(align_of::<CompactAltBlockInfo>(), 8);
/// ```
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)]
#[repr(C)]
pub struct CompactAltBlockInfo {
/// The block's hash.
pub block_hash: [u8; 32],
/// The block's proof-of-work hash.
pub pow_hash: [u8; 32],
/// The block's height.
pub height: usize,
/// The adjusted block size, in bytes.
pub weight: usize,
/// The long term block weight, which is the weight factored in with previous block weights.
pub long_term_weight: usize,
/// The low 64 bits of the cumulative difficulty.
pub cumulative_difficulty_low: u64,
/// The high 64 bits of the cumulative difficulty.
pub cumulative_difficulty_high: u64,
}
//---------------------------------------------------------------------------------------------------- AltTransactionInfo
/// Represents information on an alt transaction.
///
/// ```rust
/// # use std::borrow::*;
/// # use cuprate_blockchain::{*, types::*};
/// use cuprate_database::Storable;
///
/// // Assert Storable is correct.
/// let a: AltTransactionInfo = AltTransactionInfo {
/// tx_weight: 1,
/// fee: 6,
/// tx_hash: [6; 32],
/// };
///
/// let b = Storable::as_bytes(&a);
/// let c: AltTransactionInfo = Storable::from_bytes(b);
/// assert_eq!(a, c);
/// ```
///
/// # Size & Alignment
/// ```rust
/// # use cuprate_blockchain::types::*;
/// assert_eq!(size_of::<AltTransactionInfo>(), 48);
/// assert_eq!(align_of::<AltTransactionInfo>(), 8);
/// ```
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)]
#[repr(C)]
pub struct AltTransactionInfo {
/// The transaction's weight.
pub tx_weight: usize,
/// The transaction's total fees.
pub fee: u64,
/// The transaction's hash.
pub tx_hash: [u8; 32],
}
//---------------------------------------------------------------------------------------------------- Tests
#[cfg(test)]
mod test {

View file

@ -7,7 +7,7 @@ authors = ["Boog900", "hinto-janai"]
[dependencies]
cuprate-types = { path = "../types" }
cuprate-helper = { path = "../helper", features = ["map"] }
cuprate-helper = { path = "../helper", features = ["map", "tx"] }
cuprate-wire = { path = "../net/wire" }
cuprate-p2p-core = { path = "../p2p/p2p-core", features = ["borsh"] }
@ -22,11 +22,13 @@ tokio = { workspace = true, features = ["full"] }
tokio-util = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
bytes = { workspace = true, features = ["std"] }
tempfile = { workspace = true }
paste = { workspace = true }
borsh = { workspace = true, features = ["derive"]}
[dev-dependencies]
hex = { workspace = true }
pretty_assertions = { workspace = true }
pretty_assertions = { workspace = true }
[lints]
workspace = true

View file

@ -25,13 +25,11 @@
//! let tx: VerifiedTransactionInformation = TX_V1_SIG0.clone();
//! ```
mod constants;
pub use constants::{
BLOCK_43BD1F, BLOCK_5ECB7E, BLOCK_BBD604, BLOCK_F91043, TX_2180A8, TX_3BC7FF, TX_84D48D,
TX_9E3F73, TX_B6B439, TX_D7FEBD, TX_E2D393, TX_E57440,
};
pub use statics::{BLOCK_V16_TX0, BLOCK_V1_TX2, BLOCK_V9_TX3, TX_V1_SIG0, TX_V1_SIG2, TX_V2_RCT3};
mod constants;
mod statics;
pub use statics::{
tx_fee, BLOCK_V16_TX0, BLOCK_V1_TX2, BLOCK_V9_TX3, TX_V1_SIG0, TX_V1_SIG2, TX_V2_RCT3,
};

View file

@ -8,12 +8,12 @@
//---------------------------------------------------------------------------------------------------- Import
use std::sync::LazyLock;
use cuprate_helper::map::combine_low_high_bits_to_u128;
use cuprate_types::{VerifiedBlockInformation, VerifiedTransactionInformation};
use hex_literal::hex;
use monero_serai::transaction::Input;
use monero_serai::{block::Block, transaction::Transaction};
use cuprate_helper::{map::combine_low_high_bits_to_u128, tx::tx_fee};
use cuprate_types::{VerifiedBlockInformation, VerifiedTransactionInformation};
use crate::data::constants::{
BLOCK_43BD1F, BLOCK_5ECB7E, BLOCK_F91043, TX_2180A8, TX_3BC7FF, TX_84D48D, TX_9E3F73,
TX_B6B439, TX_D7FEBD, TX_E2D393, TX_E57440,
@ -110,36 +110,6 @@ fn to_tx_verification_data(tx_blob: impl AsRef<[u8]>) -> VerifiedTransactionInfo
}
}
/// Calculates the fee of the [`Transaction`].
///
/// # Panics
/// This will panic if the inputs overflow or the transaction outputs too much.
pub fn tx_fee(tx: &Transaction) -> u64 {
let mut fee = 0_u64;
match &tx {
Transaction::V1 { prefix, .. } => {
for input in &prefix.inputs {
match input {
Input::Gen(_) => return 0,
Input::ToKey { amount, .. } => {
fee = fee.checked_add(amount.unwrap_or(0)).unwrap();
}
}
}
for output in &prefix.outputs {
fee.checked_sub(output.amount.unwrap_or(0)).unwrap();
}
}
Transaction::V2 { proofs, .. } => {
fee = proofs.as_ref().unwrap().base.fee;
}
};
fee
}
//---------------------------------------------------------------------------------------------------- Blocks
/// Generate a `static LazyLock<VerifiedBlockInformation>`.
///
@ -148,8 +118,8 @@ pub fn tx_fee(tx: &Transaction) -> u64 {
///
/// This requires some static block/tx input (from data) and some fields.
/// This data can be accessed more easily via:
/// - A block explorer (https://xmrchain.net)
/// - Monero RPC (see cuprate_test_utils::rpc for this)
/// - A block explorer (<https://xmrchain.net>)
/// - Monero RPC (see `cuprate_test_utils::rpc` for this)
///
/// See below for actual usage.
macro_rules! verified_block_information {
@ -311,12 +281,12 @@ transaction_verification_data! {
//---------------------------------------------------------------------------------------------------- TESTS
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use crate::rpc::client::HttpRpcClient;
use super::*;
/// Assert the defined blocks are the same compared to ones received from a local RPC call.
#[ignore] // FIXME: doesn't work in CI, we need a real unrestricted node
#[tokio::test]

View file

@ -178,6 +178,7 @@ impl Drop for SpawnedMoneroD {
println!("------END-MONEROD-LOGS------");
}
#[expect(clippy::manual_assert, reason = "`if` is more clear")]
if error && !panicking() {
// `println` only outputs in a test when panicking so if there is an error while
// dropping monerod but not an error in the test then we need to panic to make sure

View file

@ -1,18 +1,16 @@
//! HTTP RPC client.
//---------------------------------------------------------------------------------------------------- Use
use monero_rpc::Rpc;
use monero_serai::block::Block;
use monero_simple_request_rpc::SimpleRequestRpc;
use serde::Deserialize;
use serde_json::json;
use tokio::task::spawn_blocking;
use monero_rpc::Rpc;
use monero_serai::block::Block;
use monero_simple_request_rpc::SimpleRequestRpc;
use cuprate_helper::tx::tx_fee;
use cuprate_types::{VerifiedBlockInformation, VerifiedTransactionInformation};
use crate::data::tx_fee;
//---------------------------------------------------------------------------------------------------- Constants
/// The default URL used for Monero RPC connections.
pub const LOCALHOST_RPC_URL: &str = "http://127.0.0.1:18081";
@ -47,13 +45,13 @@ impl HttpRpcClient {
}
/// The address used for this [`HttpRpcClient`].
#[allow(dead_code)]
#[allow(clippy::allow_attributes, dead_code, reason = "expect doesn't work")]
const fn address(&self) -> &String {
&self.address
}
/// Access to the inner RPC client for other usage.
#[allow(dead_code)]
#[expect(dead_code)]
const fn rpc(&self) -> &SimpleRequestRpc {
&self.rpc
}
@ -184,9 +182,10 @@ impl HttpRpcClient {
//---------------------------------------------------------------------------------------------------- TESTS
#[cfg(test)]
mod tests {
use super::*;
use hex_literal::hex;
use super::*;
/// Assert the default address is localhost.
#[tokio::test]
async fn localhost() {
@ -197,7 +196,7 @@ mod tests {
#[ignore] // FIXME: doesn't work in CI, we need a real unrestricted node
#[tokio::test]
async fn get() {
#[allow(clippy::too_many_arguments)]
#[expect(clippy::too_many_arguments)]
async fn assert_eq(
rpc: &HttpRpcClient,
height: usize,

View file

@ -156,13 +156,5 @@ macro_rules! define_request_and_response_doc_test {
"```\n",
)
};
// No doc test.
(
$name:ident,
$test:ident,
) => {
""
};
}
pub(super) use define_request_and_response_doc_test;

View file

@ -8,8 +8,7 @@ define_request_and_response! {
// `(other)` adds a JSON sanity-check test.
get_height (other),
GET_HEIGHT: &str,
Request =
r#"{}"#;
Request = "{}";
Response =
r#"{
"hash": "68bb1a1cff8e2a44c3221e8e1aff80bc6ca45d06fa8eff4d2a3a7ac31d4efe3f",
@ -53,8 +52,7 @@ r#"{
define_request_and_response! {
get_alt_blocks_hashes (other),
GET_ALT_BLOCKS_HASHES: &str,
Request =
r#"{}"#;
Request = "{}";
Response =
r#"{
"blks_hashes": ["8ee10db35b1baf943f201b303890a29e7d45437bd76c2bd4df0d2f2ee34be109"],
@ -134,8 +132,7 @@ r#"{
define_request_and_response! {
stop_mining (other),
STOP_MINING: &str,
Request =
r#"{}"#;
Request = "{}";
Response =
r#"{
"status": "OK",
@ -146,8 +143,7 @@ r#"{
define_request_and_response! {
mining_status (other),
MINING_STATUS: &str,
Request =
r#"{}"#;
Request = "{}";
Response =
r#"{
"active": false,
@ -173,8 +169,7 @@ r#"{
define_request_and_response! {
save_bc (other),
SAVE_BC: &str,
Request =
r#"{}"#;
Request = "{}";
Response =
r#"{
"status": "OK",
@ -185,8 +180,7 @@ r#"{
define_request_and_response! {
get_peer_list (other),
GET_PEER_LIST: &str,
Request =
r#"{}"#;
Request = "{}";
Response =
r#"{
"gray_list": [{
@ -291,8 +285,7 @@ r#"{
define_request_and_response! {
get_transaction_pool (other),
GET_TRANSACTION_POOL: &str,
Request =
r#"{}"#;
Request = "{}";
Response =
r#"{
"credits": 0,
@ -598,8 +591,7 @@ r#"{
define_request_and_response! {
get_transaction_pool_stats (other),
GET_TRANSACTION_POOL_STATS: &str,
Request =
r#"{}"#;
Request = "{}";
Response =
r#"{
"credits": 0,
@ -657,8 +649,7 @@ r#"{
define_request_and_response! {
stop_daemon (other),
STOP_DAEMON: &str,
Request =
r#"{}"#;
Request = "{}";
Response =
r#"{
"status": "OK"
@ -668,8 +659,7 @@ r#"{
define_request_and_response! {
get_limit (other),
GET_LIMIT: &str,
Request =
r#"{}"#;
Request = "{}";
Response =
r#"{
"limit_down": 1280000,
@ -713,8 +703,7 @@ r#"{
define_request_and_response! {
get_net_stats (other),
GET_NET_STATS: &str,
Request =
r#"{}"#;
Request = "{}";
Response =
r#"{
"start_time": 1721251858,
@ -801,8 +790,7 @@ r#"{
define_request_and_response! {
UNDOCUMENTED_ENDPOINT (other),
GET_TRANSACTION_POOL_HASHES: &str,
Request =
r#"{}"#;
Request = "{}";
Response =
r#"{
"credits": 0,
@ -835,8 +823,7 @@ r#"{
define_request_and_response! {
UNDOCUMENTED_ENDPOINT (other),
GET_PUBLIC_NODES: &str,
Request =
r#"{}"#;
Request = "{}";
Response =
r#"{
"status": "OK",

View file

@ -86,9 +86,8 @@ impl<const ALLOW_SYNC: bool, const DANDELION_PP: bool, const CHECK_NODE_ID: bool
type Sink = FramedWrite<WriteHalf<DuplexStream>, MoneroWireCodec>;
type Listener = Pin<
Box<
dyn Stream<
Item = Result<(Option<Self::Addr>, Self::Stream, Self::Sink), std::io::Error>,
> + Send
dyn Stream<Item = Result<(Option<Self::Addr>, Self::Stream, Self::Sink), Error>>
+ Send
+ 'static,
>,
>;

View file

@ -2,14 +2,16 @@
//!
//! Tests that assert particular requests lead to particular
//! responses are also tested in Cuprate's blockchain database crate.
//---------------------------------------------------------------------------------------------------- Import
use std::{
collections::{HashMap, HashSet},
ops::Range,
};
use crate::types::{Chain, ExtendedBlockHeader, OutputOnChain, VerifiedBlockInformation};
use crate::{
types::{Chain, ExtendedBlockHeader, OutputOnChain, VerifiedBlockInformation},
AltBlockInformation, ChainId,
};
//---------------------------------------------------------------------------------------------------- ReadRequest
/// A read request to the blockchain database.
@ -92,26 +94,49 @@ pub enum BlockchainReadRequest {
CompactChainHistory,
/// A request to find the first unknown block ID in a list of block IDs.
////
///
/// # Invariant
/// The [`Vec`] containing the block IDs must be sorted in chronological block
/// order, or else the returned response is unspecified and meaningless,
/// as this request performs a binary search.
FindFirstUnknown(Vec<[u8; 32]>),
/// A request for all alt blocks in the chain with the given [`ChainId`].
AltBlocksInChain(ChainId),
}
//---------------------------------------------------------------------------------------------------- WriteRequest
/// A write request to the blockchain database.
///
/// There is currently only 1 write request to the database,
/// as such, the only valid [`BlockchainResponse`] to this request is
/// the proper response for a [`BlockchainResponse::WriteBlockOk`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BlockchainWriteRequest {
/// Request that a block be written to the database.
///
/// Input is an already verified block.
WriteBlock(VerifiedBlockInformation),
/// Write an alternative block to the database,
///
/// Input is the alternative block.
WriteAltBlock(AltBlockInformation),
/// A request to pop some blocks from the top of the main chain
///
/// Input is the amount of blocks to pop.
///
/// This request flushes all alt-chains from the cache before adding the popped blocks to the
/// alt cache.
PopBlocks(usize),
/// A request to reverse the re-org process.
///
/// The inner value is the [`ChainId`] of the old main chain.
///
/// # Invariant
/// It is invalid to call this with a [`ChainId`] that was not returned from [`BlockchainWriteRequest::PopBlocks`].
ReverseReorg(ChainId),
/// A request to flush all alternative blocks.
FlushAltBlocks,
}
//---------------------------------------------------------------------------------------------------- Response
@ -197,12 +222,24 @@ pub enum BlockchainResponse {
/// This will be [`None`] if all blocks were known.
FindFirstUnknown(Option<(usize, usize)>),
//------------------------------------------------------ Writes
/// Response to [`BlockchainWriteRequest::WriteBlock`].
/// The response for [`BlockchainReadRequest::AltBlocksInChain`].
///
/// This response indicates that the requested block has
/// successfully been written to the database without error.
WriteBlockOk,
/// Contains all the alt blocks in the alt-chain in chronological order.
AltBlocksInChain(Vec<AltBlockInformation>),
//------------------------------------------------------ Writes
/// A generic Ok response to indicate a request was successfully handled.
///
/// currently the response for:
/// - [`BlockchainWriteRequest::WriteBlock`]
/// - [`BlockchainWriteRequest::WriteAltBlock`]
/// - [`BlockchainWriteRequest::ReverseReorg`]
/// - [`BlockchainWriteRequest::FlushAltBlocks`]
Ok,
/// The response for [`BlockchainWriteRequest::PopBlocks`].
///
/// The inner value is the alt-chain ID for the old main chain blocks.
PopBlocks(ChainId),
}
//---------------------------------------------------------------------------------------------------- Tests

View file

@ -1,6 +1,8 @@
//! Various shared data types in Cuprate.
//---------------------------------------------------------------------------------------------------- Import
use std::num::NonZero;
use curve25519_dalek::edwards::EdwardsPoint;
use monero_serai::{
block::Block,
@ -38,8 +40,7 @@ pub struct ExtendedBlockHeader {
//---------------------------------------------------------------------------------------------------- VerifiedTransactionInformation
/// Verified information of a transaction.
///
/// - If this is in a [`VerifiedBlockInformation`] this represents a valid transaction
/// - If this is in an [`AltBlockInformation`] this represents a potentially valid transaction
/// This represents a valid transaction
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VerifiedTransactionInformation {
/// The transaction itself.
@ -79,6 +80,7 @@ pub struct VerifiedBlockInformation {
/// [`Block::hash`].
pub block_hash: [u8; 32],
/// The block's proof-of-work hash.
// TODO: make this an option.
pub pow_hash: [u8; 32],
/// The block's height.
pub height: usize,
@ -97,7 +99,7 @@ pub struct VerifiedBlockInformation {
///
/// The inner value is meaningless.
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub struct ChainId(pub u64);
pub struct ChainId(pub NonZero<u64>);
//---------------------------------------------------------------------------------------------------- Chain
/// An identifier for a chain.