init consensus rules crate

This commit is contained in:
Boog900 2023-09-03 23:50:38 +01:00
parent 59e7e0b4e8
commit 2f08978e67
No known key found for this signature in database
GPG key ID: 5401367FB7302004
9 changed files with 629 additions and 9 deletions

View file

@ -2,7 +2,8 @@
[workspace] [workspace]
members = [ members = [
# "common", "common",
"consensus",
#"cuprate", #"cuprate",
# "database", # "database",
"net/levin", "net/levin",

View file

@ -1,8 +1,8 @@
pub mod hardforks; //pub mod hardforks;
pub mod network; pub mod network;
pub mod pruning; pub mod pruning;
pub use hardforks::HardForks; //pub use hardforks::HardForks;
pub use network::Network; pub use network::Network;
pub use pruning::{PruningError, PruningSeed}; pub use pruning::{PruningError, PruningSeed};
@ -12,3 +12,15 @@ pub const CRYPTONOTE_MAX_BLOCK_NUMBER: u64 = 500000000;
pub const CRYPTONOTE_PRUNING_LOG_STRIPES: u32 = 3; pub const CRYPTONOTE_PRUNING_LOG_STRIPES: u32 = 3;
pub const CRYPTONOTE_PRUNING_STRIPE_SIZE: u64 = 4096; pub const CRYPTONOTE_PRUNING_STRIPE_SIZE: u64 = 4096;
pub const CRYPTONOTE_PRUNING_TIP_BLOCKS: u64 = 5500; pub const CRYPTONOTE_PRUNING_TIP_BLOCKS: u64 = 5500;
#[derive(Debug)]
pub enum BlockID {
Hash([u8; 32]),
Height(u64),
}
impl From<u64> for BlockID {
fn from(value: u64) -> Self {
BlockID::Height(value)
}
}

View file

@ -10,17 +10,17 @@ const STAGENET_NETWORK_ID: [u8; 16] = [
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum Network { pub enum Network {
MainNet, Mainnet,
TestNet, Testnet,
StageNet, Stagenet,
} }
impl Network { impl Network {
pub fn network_id(&self) -> [u8; 16] { pub fn network_id(&self) -> [u8; 16] {
match self { match self {
Network::MainNet => MAINNET_NETWORK_ID, Network::Mainnet => MAINNET_NETWORK_ID,
Network::TestNet => TESTNET_NETWORK_ID, Network::Testnet => TESTNET_NETWORK_ID,
Network::StageNet => STAGENET_NETWORK_ID, Network::Stagenet => STAGENET_NETWORK_ID,
} }
} }
} }

31
consensus/Cargo.toml Normal file
View file

@ -0,0 +1,31 @@
[package]
name = "monero-consensus"
version = "0.1.0"
edition = "2021"
description = "A crate implimenting all Moneros consensus rules."
license = "MIT"
authors = ["Boog900"]
repository = "https://github.com/Cuprate/cuprate/tree/main/consensus"
[features]
default = ["binaries"]
binaries = ["rpc", "dep:tokio", "dep:tracing-subscriber", "tower/retry", "tower/balance"]
rpc = ["dep:futures"]
[dependencies]
hex = "0.4"
thiserror = "1"
tower = {version = "0.4", features = ["util"]}
tracing = "0.1"
monero-serai = {git="https://github.com/Cuprate/serai.git", rev = "84b77b1"}
cuprate-common = {path = "../common"}
# used for rpc
futures = {version = "0.3", optional = true}
# used in binaries
tokio = { version = "1", features = ["rt-multi-thread", "macros"], optional = true }
tracing-subscriber = {version = "0.3", optional = true}
# here to help cargo to pick a version - remove me
syn = "2.0.29"

View file

@ -0,0 +1,39 @@
#![cfg(feature = "binaries")]
use tower::{Service, ServiceExt};
use tracing::level_filters::LevelFilter;
use monero_consensus::hardforks::{HardFork, HardForkConfig, HardForks};
use monero_consensus::rpc::Rpc;
use monero_consensus::DatabaseRequest;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_max_level(LevelFilter::INFO)
.init();
let mut rpc = Rpc::new_http("http://xmr-node.cakewallet.com:18081".to_string());
let res = rpc
.ready()
.await
.unwrap()
.call(DatabaseRequest::ChainHeight)
.await
.unwrap();
println!("{:?}", res);
let mut hfs = HardForks::init(HardForkConfig::default(), &mut rpc)
.await
.unwrap();
println!("{:?}", hfs);
hfs.new_block(HardFork::V2, 1009827, &mut rpc).await;
println!("{:?}", hfs);
hfs.new_block(HardFork::V2, 1009828, &mut rpc).await;
println!("{:?}", hfs);
}

70
consensus/src/genesis.rs Normal file
View file

@ -0,0 +1,70 @@
/// This module contains the code to generate Monero's genesis blocks.
///
/// ref: consensus-doc#Genesis
use monero_serai::{
block::{Block, BlockHeader},
transaction::Transaction,
};
use cuprate_common::Network;
fn genesis_nonce(network: &Network) -> u32 {
match network {
Network::Mainnet => 10000,
Network::Testnet => 10001,
Network::Stagenet => 10002,
}
}
fn genesis_miner_tx(network: &Network) -> Transaction {
Transaction::read(&mut hex::decode(match network {
Network::Mainnet | Network::Testnet => "013c01ff0001ffffffffffff03029b2e4c0281c0b02e7c53291a94d1d0cbff8883f8024f5142ee494ffbbd08807121017767aafcde9be00dcfd098715ebcf7f410daebc582fda69d24a28e9d0bc890d1",
Network::Stagenet => "013c01ff0001ffffffffffff0302df5d56da0c7d643ddd1ce61901c7bdc5fb1738bfe39fbe69c28a3a7032729c0f2101168d0c4ca86fb55a4cf6a36d31431be1c53a3bd7411bb24e8832410289fa6f3b"
}).unwrap().as_slice()).unwrap()
}
/// Generates the Monero genesis block.
///
/// ref: consensus-doc#Genesis
pub fn generate_genesis_block(network: &Network) -> Block {
Block {
header: BlockHeader {
major_version: 1,
minor_version: 0,
timestamp: 0,
previous: [0; 32],
nonce: genesis_nonce(network),
},
miner_tx: genesis_miner_tx(network),
txs: vec![],
}
}
#[cfg(test)]
mod tests {
use cuprate_common::Network;
use super::generate_genesis_block;
#[test]
fn generate_genesis_blocks() {
assert_eq!(
&generate_genesis_block(&Network::Mainnet).hash(),
hex::decode("418015bb9ae982a1975da7d79277c2705727a56894ba0fb246adaabb1f4632e3")
.unwrap()
.as_slice()
);
assert_eq!(
&generate_genesis_block(&Network::Testnet).hash(),
hex::decode("48ca7cd3c8de5b6a4d53d2861fbdaedca141553559f9be9520068053cda8430b")
.unwrap()
.as_slice()
);
assert_eq!(
&generate_genesis_block(&Network::Stagenet).hash(),
hex::decode("76ee3cc98646292206cd3e86f74d88b4dcc1d937088645e9b0cbca84b7ce74eb")
.unwrap()
.as_slice()
);
}
}

342
consensus/src/hardforks.rs Normal file
View file

@ -0,0 +1,342 @@
use std::ops::Range;
use monero_serai::block::BlockHeader;
use tower::ServiceExt;
use tracing::instrument;
use cuprate_common::{BlockID, Network};
use crate::{Database, DatabaseRequest, DatabaseResponse, Error};
//http://localhost:3000/consensus_rules/hardforks.html#window-size
const DEFAULT_WINDOW_SIZE: u64 = 10080; // supermajority window check length - a week
/// An identifier for every hard-fork Monero has had.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
#[repr(u8)]
pub enum HardFork {
V1 = 1,
V2,
V3,
V4,
V5,
V6,
V7,
V8,
V9,
V10,
V11,
V12,
V13,
V14,
V15,
V16,
}
impl HardFork {
/// Returns the hard-fork for a blocks `major_version` field.
///
/// http://**/consensus_rules/hardforks.html#blocks-version-and-vote
pub fn from_version(version: &u8) -> Result<HardFork, Error> {
Ok(match version {
1 => HardFork::V1,
2 => HardFork::V2,
3 => HardFork::V3,
4 => HardFork::V4,
5 => HardFork::V5,
6 => HardFork::V6,
7 => HardFork::V7,
8 => HardFork::V8,
9 => HardFork::V9,
10 => HardFork::V10,
11 => HardFork::V11,
12 => HardFork::V12,
13 => HardFork::V13,
14 => HardFork::V14,
15 => HardFork::V15,
16 => HardFork::V16,
_ => {
return Err(Error::InvalidHardForkVersion(
"Version is not a known hard fork",
))
}
})
}
/// Returns the hard-fork for a blocks `minor_version` (vote) field.
///
/// http://**/consensus_rules/hardforks.html#blocks-version-and-vote
pub fn from_vote(vote: &u8) -> HardFork {
if *vote == 0 {
// A vote of 0 is interpreted as 1 as that's what Monero used to default to.
return HardFork::V1;
}
// This must default to the latest hard-fork!
Self::from_version(vote).unwrap_or(HardFork::V16)
}
/// Returns the next hard-fork.
pub fn next_fork(&self) -> Option<HardFork> {
match self {
HardFork::V1 => Some(HardFork::V2),
HardFork::V2 => Some(HardFork::V3),
HardFork::V3 => Some(HardFork::V4),
HardFork::V4 => Some(HardFork::V5),
HardFork::V5 => Some(HardFork::V6),
HardFork::V6 => Some(HardFork::V7),
HardFork::V7 => Some(HardFork::V8),
HardFork::V8 => Some(HardFork::V9),
HardFork::V9 => Some(HardFork::V10),
HardFork::V10 => Some(HardFork::V11),
HardFork::V11 => Some(HardFork::V12),
HardFork::V12 => Some(HardFork::V13),
HardFork::V13 => Some(HardFork::V14),
HardFork::V14 => Some(HardFork::V15),
HardFork::V15 => Some(HardFork::V16),
HardFork::V16 => None,
}
}
/// Returns the threshold of this fork.
pub fn fork_threshold(&self, _: &Network) -> u64 {
0
}
/// Returns the votes needed for this fork.
pub fn votes_needed(&self, network: &Network, window: u64) -> u64 {
(self.fork_threshold(network) * window + 99) / 100
}
/// Returns the minimum height this fork will activate at
pub fn fork_height(&self, network: &Network) -> u64 {
match network {
Network::Mainnet => self.mainnet_fork_height(),
Network::Stagenet => self.stagenet_fork_height(),
Network::Testnet => self.testnet_fork_height(),
}
}
fn stagenet_fork_height(&self) -> u64 {
todo!()
}
fn testnet_fork_height(&self) -> u64 {
todo!()
}
fn mainnet_fork_height(&self) -> u64 {
match self {
HardFork::V1 => 0, // Monero core has this as 1, which is strange
HardFork::V2 => 1009827,
HardFork::V3 => 1141317,
HardFork::V4 => 1220516,
HardFork::V5 => 1288616,
HardFork::V6 => 1400000,
HardFork::V7 => 1546000,
HardFork::V8 => 1685555,
HardFork::V9 => 1686275,
HardFork::V10 => 1788000,
HardFork::V11 => 1788720,
HardFork::V12 => 1978433,
HardFork::V13 => 2210000,
HardFork::V14 => 2210720,
HardFork::V15 => 2688888,
HardFork::V16 => 2689608,
}
}
}
/// A struct holding the current voting state of the blockchain.
#[derive(Debug, Default)]
struct HFVotes {
votes: [u64; 16],
}
impl HFVotes {
/// Add votes for a hard-fork
pub fn add_votes_for_hf(&mut self, hf: &HardFork, votes: u64) {
self.votes[*hf as usize - 1] += votes;
}
/// Add a vote for a hard-fork.
pub fn add_vote_for_hf(&mut self, hf: &HardFork) {
self.add_votes_for_hf(hf, 1)
}
/// Remove a vote for a hard-fork.
pub fn remove_vote_for_hf(&mut self, hf: &HardFork) {
self.votes[*hf as usize - 1] -= 1;
}
/// Returns the total votes for a hard-fork.
///
/// http://localhost:3000/consensus_rules/hardforks.html#accepting-a-fork
pub fn get_votes_for_hf(&self, hf: &HardFork) -> u64 {
self.votes[*hf as usize - 1..].iter().sum()
}
/// Returns the total amount of votes being tracked
pub fn total_votes(&self) -> u64 {
self.votes.iter().sum()
}
}
/// Configuration for hard-forks.
///
#[derive(Debug)]
pub struct HardForkConfig {
/// The network we are on.
network: Network,
/// The amount of votes we are taking into account to decide on a fork activation.
window: u64,
}
impl Default for HardForkConfig {
fn default() -> Self {
Self {
network: Network::Mainnet,
window: 3, //DEFAULT_WINDOW_SIZE,
}
}
}
/// A struct that keeps track of the current hard-fork and current votes.
#[derive(Debug)]
pub struct HardForks {
current_hardfork: HardFork,
next_hardfork: Option<HardFork>,
config: HardForkConfig,
votes: HFVotes,
last_height: u64,
}
impl HardForks {
pub async fn init<D>(config: HardForkConfig, database: &mut D) -> Result<Self, Error>
where
D: Database,
{
let DatabaseResponse::ChainHeight(chain_height) = database
.ready()
.await?
.call(DatabaseRequest::ChainHeight)
.await? else {
panic!("Database sent incorrect response")
};
let block_heights = if chain_height > config.window {
chain_height - config.window..chain_height
} else {
0..chain_height
};
let votes = get_votes_in_range(database, block_heights).await?;
if chain_height > config.window {
assert_eq!(votes.total_votes(), config.window)
}
let latest_header = get_block_header(database, chain_height - 1).await?;
let current_hardfork = HardFork::from_version(&latest_header.major_version)
.expect("Invalid major version in stored block");
let next_hardfork = current_hardfork.next_fork();
let mut hfs = HardForks {
config,
current_hardfork,
next_hardfork,
votes,
last_height: chain_height - 1,
};
// chain_height = height + 1
hfs.check_set_new_hf(chain_height);
Ok(hfs)
}
pub fn check_block_version_vote(&self, version: &HardFork, vote: &HardFork) -> bool {
&self.current_hardfork == version && vote >= &self.current_hardfork
}
pub async fn new_block<D: Database>(&mut self, vote: HardFork, height: u64, database: &mut D) {
assert_eq!(self.last_height + 1, height);
self.last_height += 1;
self.votes.add_vote_for_hf(&vote);
for offset in self.config.window..self.votes.total_votes() {
let header = get_block_header(database, height - offset)
.await
.expect("Error retrieving block we should have in database");
self.votes
.remove_vote_for_hf(&HardFork::from_vote(&header.minor_version));
}
if height > self.config.window {
assert_eq!(self.votes.total_votes(), self.config.window);
}
self.check_set_new_hf(height + 1)
}
fn check_set_new_hf(&mut self, height: u64) {
while let Some(new_hf) = self.next_hardfork {
if height >= new_hf.fork_height(&self.config.network)
&& self.votes.get_votes_for_hf(&new_hf)
>= new_hf.votes_needed(&self.config.network, self.config.window)
{
self.set_hf(new_hf);
} else {
return;
}
}
}
fn set_hf(&mut self, new_hf: HardFork) {
self.next_hardfork = new_hf.next_fork();
self.current_hardfork = new_hf;
}
}
#[instrument(skip(database))]
async fn get_votes_in_range<D: Database>(
database: &mut D,
block_heights: Range<u64>,
) -> Result<HFVotes, Error> {
let mut votes = HFVotes::default();
for height in block_heights {
let header = get_block_header(database, height).await?;
let vote = HardFork::from_vote(&header.minor_version);
tracing::info!("Block vote for height: {} = {:?}", height, vote);
votes.add_vote_for_hf(&HardFork::from_vote(&header.minor_version));
}
Ok(votes)
}
async fn get_block_header<D: Database>(
database: &mut D,
block_id: impl Into<BlockID>,
) -> Result<BlockHeader, Error> {
let DatabaseResponse::BlockHeader(header) = database
.oneshot(DatabaseRequest::BlockHeader(block_id.into()))
.await? else {
panic!("Database sent incorrect response for block header request")
};
Ok(header)
}
#[test]
fn to_from_hf() {
let hf = HardFork::V1 as u8;
assert_eq!(hf, 1)
}

36
consensus/src/lib.rs Normal file
View file

@ -0,0 +1,36 @@
use tower::ServiceExt;
pub mod genesis;
pub mod hardforks;
#[cfg(feature = "rpc")]
pub mod rpc;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Invalid hard fork version: {0}")]
InvalidHardForkVersion(&'static str),
#[error("Database error: {0}")]
Database(#[from] tower::BoxError),
}
pub trait Database:
tower::Service<DatabaseRequest, Response = DatabaseResponse, Error = tower::BoxError>
{
}
impl<T: tower::Service<DatabaseRequest, Response = DatabaseResponse, Error = tower::BoxError>>
Database for T
{
}
#[derive(Debug)]
pub enum DatabaseRequest {
BlockHeader(cuprate_common::BlockID),
ChainHeight,
}
#[derive(Debug)]
pub enum DatabaseResponse {
BlockHeader(monero_serai::block::BlockHeader),
ChainHeight(u64),
}

89
consensus/src/rpc.rs Normal file
View file

@ -0,0 +1,89 @@
use futures::lock::{OwnedMutexGuard, OwnedMutexLockFuture};
use futures::{FutureExt, TryFutureExt};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use monero_serai::rpc::{HttpRpc, RpcConnection};
use cuprate_common::BlockID;
use crate::{DatabaseRequest, DatabaseResponse};
enum RpcState<R: RpcConnection> {
Locked,
Acquiring(OwnedMutexLockFuture<monero_serai::rpc::Rpc<R>>),
Acquired(OwnedMutexGuard<monero_serai::rpc::Rpc<R>>),
}
pub struct Rpc<R: RpcConnection>(
Arc<futures::lock::Mutex<monero_serai::rpc::Rpc<R>>>,
RpcState<R>,
);
impl Rpc<HttpRpc> {
pub fn new_http(addr: String) -> Rpc<HttpRpc> {
let http_rpc = HttpRpc::new(addr).unwrap();
Rpc(
Arc::new(futures::lock::Mutex::new(http_rpc)),
RpcState::Locked,
)
}
}
impl<R: RpcConnection> Clone for Rpc<R> {
fn clone(&self) -> Self {
Rpc(Arc::clone(&self.0), RpcState::Locked)
}
}
impl<R: RpcConnection + Send + Sync + 'static> tower::Service<DatabaseRequest> for Rpc<R> {
type Response = DatabaseResponse;
type Error = tower::BoxError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + 'static>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
loop {
match &mut self.1 {
RpcState::Locked => self.1 = RpcState::Acquiring(self.0.clone().lock_owned()),
RpcState::Acquiring(rpc) => {
self.1 = RpcState::Acquired(futures::ready!(rpc.poll_unpin(cx)))
}
RpcState::Acquired(_) => return Poll::Ready(Ok(())),
}
}
}
fn call(&mut self, req: DatabaseRequest) -> Self::Future {
let RpcState::Acquired(rpc) = std::mem::replace(&mut self.1, RpcState::Locked) else {
panic!("poll_ready was not called first!");
};
match req {
DatabaseRequest::ChainHeight => async move {
rpc.get_height()
.map_ok(|height| DatabaseResponse::ChainHeight(height.try_into().unwrap()))
.map_err(Into::into)
.await
}
.boxed(),
DatabaseRequest::BlockHeader(id) => match id {
BlockID::Hash(hash) => async move {
rpc.get_block(hash)
.map_ok(|block| DatabaseResponse::BlockHeader(block.header))
.map_err(Into::into)
.await
}
.boxed(),
BlockID::Height(height) => async move {
rpc.get_block_by_number(height.try_into().unwrap())
.map_ok(|block| DatabaseResponse::BlockHeader(block.header))
.map_err(Into::into)
.await
}
.boxed(),
},
}
}
}