mirror of
https://github.com/Cuprate/cuprate.git
synced 2024-12-22 19:49:28 +00:00
change cuprate-consensus to use monero-consensus
This commit is contained in:
parent
b01314ff70
commit
b0588fad2b
32 changed files with 567 additions and 1888 deletions
|
@ -27,7 +27,8 @@ binaries = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hex = "0.4"
|
monero-consensus = {path = "./rules", features = ["rayon"]}
|
||||||
|
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
tower = {version = "0.4", features = ["util"]}
|
tower = {version = "0.4", features = ["util"]}
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
@ -37,18 +38,19 @@ crypto-bigint = "0.5"
|
||||||
curve25519-dalek = "4"
|
curve25519-dalek = "4"
|
||||||
|
|
||||||
randomx-rs = "1"
|
randomx-rs = "1"
|
||||||
monero-serai = {git="https://github.com/cuprate/serai.git", rev = "4a5d860"}
|
monero-serai = { workspace = true }
|
||||||
multiexp = {git="https://github.com/cuprate/serai.git", rev = "4a5d860"}
|
multiexp = {git="https://github.com/cuprate/serai.git", rev = "4a5d860"}
|
||||||
dalek-ff-group = {git="https://github.com/cuprate/serai.git", rev = "4a5d860"}
|
dalek-ff-group = {git="https://github.com/cuprate/serai.git", rev = "4a5d860"}
|
||||||
|
|
||||||
cuprate-common = {path = "../common"}
|
cuprate-common = {path = "../common"}
|
||||||
cryptonight-cuprate = {path = "../cryptonight"}
|
|
||||||
|
|
||||||
rayon = "1"
|
rayon = "1"
|
||||||
thread_local = "1.1.7"
|
thread_local = "1.1.7"
|
||||||
tokio = "1"
|
tokio = "1"
|
||||||
tokio-util = "0.7"
|
tokio-util = "0.7"
|
||||||
|
|
||||||
|
hex = "0.4"
|
||||||
|
|
||||||
# used in binaries
|
# used in binaries
|
||||||
monero-wire = {path="../net/monero-wire", optional = true}
|
monero-wire = {path="../net/monero-wire", optional = true}
|
||||||
monero-epee-bin-serde = {git = "https://github.com/monero-rs/monero-epee-bin-serde.git", rev = "e4a585a", optional = true}
|
monero-epee-bin-serde = {git = "https://github.com/monero-rs/monero-epee-bin-serde.git", rev = "e4a585a", optional = true}
|
||||||
|
|
|
@ -9,11 +9,16 @@ proptest = ["dep:proptest", "dep:proptest-derive"]
|
||||||
rayon = ["dep:rayon"]
|
rayon = ["dep:rayon"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
cryptonight-cuprate = {path = "../../cryptonight"}
|
||||||
|
cuprate-common = {path = "../../common"}
|
||||||
|
|
||||||
monero-serai = { workspace = true }
|
monero-serai = { workspace = true }
|
||||||
curve25519-dalek = { workspace = true }
|
curve25519-dalek = { workspace = true }
|
||||||
|
|
||||||
tracing = { workspace = true }
|
hex = "0.4"
|
||||||
|
primitive-types = { version = "0.12.2", default-features = false }
|
||||||
|
|
||||||
|
tracing = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
|
||||||
rayon = { workspace = true, optional = true }
|
rayon = { workspace = true, optional = true }
|
||||||
|
|
186
consensus/rules/src/blocks.rs
Normal file
186
consensus/rules/src/blocks.rs
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
use monero_serai::block::Block;
|
||||||
|
use primitive_types::U256;
|
||||||
|
|
||||||
|
use cryptonight_cuprate::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
current_time,
|
||||||
|
hard_forks::HardForkError,
|
||||||
|
miner_tx::{check_miner_tx, MinerTxError},
|
||||||
|
HardFork,
|
||||||
|
};
|
||||||
|
|
||||||
|
const BLOCK_SIZE_SANITY_LEEWAY: usize = 100;
|
||||||
|
const BLOCK_FUTURE_TIME_LIMIT: u64 = 60 * 60 * 2;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||||
|
pub enum BlockError {
|
||||||
|
#[error("The blocks POW is invalid.")]
|
||||||
|
POWInvalid,
|
||||||
|
#[error("The block is too big.")]
|
||||||
|
TooLarge,
|
||||||
|
#[error("The block has too many transactions.")]
|
||||||
|
TooManyTxs,
|
||||||
|
#[error("The blocks previous ID is incorrect.")]
|
||||||
|
PreviousIDIncorrect,
|
||||||
|
#[error("The blocks timestamp is invalid.")]
|
||||||
|
TimeStampInvalid,
|
||||||
|
#[error("Hard-fork error: {0}")]
|
||||||
|
HardForkError(#[from] HardForkError),
|
||||||
|
#[error("Miner transaction error: {0}")]
|
||||||
|
MinerTxError(#[from] MinerTxError),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the POW hash of this block.
|
||||||
|
pub fn calculate_pow_hash(buf: &[u8], height: u64, hf: &HardFork) -> Result<[u8; 32], BlockError> {
|
||||||
|
if height == 202612 {
|
||||||
|
return Ok(
|
||||||
|
hex::decode("84f64766475d51837ac9efbef1926486e58563c95a19fef4aec3254f03000000")
|
||||||
|
.unwrap()
|
||||||
|
.try_into()
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(if hf < &HardFork::V7 {
|
||||||
|
cryptonight_hash_v0(buf)
|
||||||
|
} else if hf == &HardFork::V7 {
|
||||||
|
cryptonight_hash_v1(buf).map_err(|_| BlockError::POWInvalid)?
|
||||||
|
} else if hf < &HardFork::V10 {
|
||||||
|
cryptonight_hash_v2(buf)
|
||||||
|
} else if hf < &HardFork::V12 {
|
||||||
|
cryptonight_hash_r(buf, height)
|
||||||
|
} else {
|
||||||
|
todo!("RandomX")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns if the blocks POW hash is valid for the current difficulty.
|
||||||
|
///
|
||||||
|
/// See: https://cuprate.github.io/monero-book/consensus_rules/blocks/difficulty.html#checking-a-blocks-proof-of-work
|
||||||
|
pub fn check_block_pow(hash: &[u8; 32], difficulty: u128) -> Result<(), BlockError> {
|
||||||
|
let int_hash = U256::from_little_endian(hash);
|
||||||
|
|
||||||
|
let difficulty = U256::from(difficulty);
|
||||||
|
|
||||||
|
if int_hash.checked_mul(difficulty).is_none() {
|
||||||
|
Err(BlockError::POWInvalid)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sanity check on the block blob size.
|
||||||
|
///
|
||||||
|
/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#block-weight-and-size
|
||||||
|
fn block_size_sanity_check(
|
||||||
|
block_blob_len: usize,
|
||||||
|
effective_median: usize,
|
||||||
|
) -> Result<(), BlockError> {
|
||||||
|
if block_blob_len > effective_median * 2 + BLOCK_SIZE_SANITY_LEEWAY {
|
||||||
|
Err(BlockError::TooLarge)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sanity check on number of txs in the block.
|
||||||
|
///
|
||||||
|
/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#amount-of-transactions
|
||||||
|
fn check_amount_txs(number_none_miner_txs: usize) -> Result<(), BlockError> {
|
||||||
|
if number_none_miner_txs + 1 > 0x10000000 {
|
||||||
|
Err(BlockError::TooManyTxs)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sanity check on the block weight.
|
||||||
|
///
|
||||||
|
/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#block-weight-and-siz
|
||||||
|
fn check_block_weight(
|
||||||
|
block_weight: usize,
|
||||||
|
median_for_block_reward: usize,
|
||||||
|
) -> Result<(), BlockError> {
|
||||||
|
if block_weight > median_for_block_reward * 2 {
|
||||||
|
Err(BlockError::TooLarge)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifies the previous id is the last blocks hash
|
||||||
|
///
|
||||||
|
/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#previous-id
|
||||||
|
fn check_prev_id(block: &Block, top_hash: &[u8; 32]) -> Result<(), BlockError> {
|
||||||
|
if &block.header.previous != top_hash {
|
||||||
|
Err(BlockError::PreviousIDIncorrect)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks the blocks timestamp is in the valid range.
|
||||||
|
///
|
||||||
|
/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#timestamp
|
||||||
|
fn check_timestamp(block: &Block, median_timestamp: u64) -> Result<(), BlockError> {
|
||||||
|
if block.header.timestamp < median_timestamp
|
||||||
|
|| block.header.timestamp > current_time() + BLOCK_FUTURE_TIME_LIMIT
|
||||||
|
{
|
||||||
|
Err(BlockError::TimeStampInvalid)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ContextToVerifyBlock {
|
||||||
|
pub median_weight_for_block_reward: usize,
|
||||||
|
pub effective_median_weight: usize,
|
||||||
|
pub top_hash: [u8; 32],
|
||||||
|
pub median_block_timestamp: Option<u64>,
|
||||||
|
pub chain_height: u64,
|
||||||
|
pub current_hf: HardFork,
|
||||||
|
pub next_difficulty: u128,
|
||||||
|
pub already_generated_coins: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks the block is valid returning the blocks hard-fork vote and the amount of coins generated.
|
||||||
|
///
|
||||||
|
/// Does not check the proof of work as that check is expensive and should be done last.
|
||||||
|
pub fn check_block(
|
||||||
|
block: &Block,
|
||||||
|
total_fees: u64,
|
||||||
|
block_weight: usize,
|
||||||
|
block_blob_len: usize,
|
||||||
|
block_chain_ctx: &ContextToVerifyBlock,
|
||||||
|
) -> Result<(HardFork, u64), BlockError> {
|
||||||
|
let (version, vote) = HardFork::from_block_header(&block.header)?;
|
||||||
|
|
||||||
|
block_chain_ctx
|
||||||
|
.current_hf
|
||||||
|
.check_block_version_vote(&version, &vote)?;
|
||||||
|
|
||||||
|
if let Some(median_timestamp) = block_chain_ctx.median_block_timestamp {
|
||||||
|
check_timestamp(block, median_timestamp)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
check_prev_id(block, &block_chain_ctx.top_hash)?;
|
||||||
|
|
||||||
|
check_block_weight(block_weight, block_chain_ctx.median_weight_for_block_reward)?;
|
||||||
|
block_size_sanity_check(block_blob_len, block_chain_ctx.effective_median_weight)?;
|
||||||
|
|
||||||
|
check_amount_txs(block.txs.len())?;
|
||||||
|
|
||||||
|
let generated_coins = check_miner_tx(
|
||||||
|
&block.miner_tx,
|
||||||
|
total_fees,
|
||||||
|
block_chain_ctx.chain_height,
|
||||||
|
block_weight,
|
||||||
|
block_chain_ctx.median_weight_for_block_reward,
|
||||||
|
block_chain_ctx.already_generated_coins,
|
||||||
|
&block_chain_ctx.current_hf,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok((vote, generated_coins))
|
||||||
|
}
|
|
@ -6,6 +6,7 @@
|
||||||
//! This module also contains a [`HFVotes`] struct which keeps track of current blockchain voting, and
|
//! This module also contains a [`HFVotes`] struct which keeps track of current blockchain voting, and
|
||||||
//! has a method [`HFVotes::check_next_hard_fork`] to check if the next hard-fork should be activated.
|
//! has a method [`HFVotes::check_next_hard_fork`] to check if the next hard-fork should be activated.
|
||||||
//!
|
//!
|
||||||
|
use monero_serai::block::BlockHeader;
|
||||||
use std::{
|
use std::{
|
||||||
collections::VecDeque,
|
collections::VecDeque,
|
||||||
fmt::{Display, Formatter},
|
fmt::{Display, Formatter},
|
||||||
|
@ -103,7 +104,7 @@ impl HardFork {
|
||||||
/// Returns the hard-fork for a blocks `major_version` field.
|
/// Returns the hard-fork for a blocks `major_version` field.
|
||||||
///
|
///
|
||||||
/// https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#blocks-version-and-vote
|
/// https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#blocks-version-and-vote
|
||||||
pub fn from_version(version: &u8) -> Result<HardFork, HardForkError> {
|
pub fn from_version(version: u8) -> Result<HardFork, HardForkError> {
|
||||||
Ok(match version {
|
Ok(match version {
|
||||||
1 => HardFork::V1,
|
1 => HardFork::V1,
|
||||||
2 => HardFork::V2,
|
2 => HardFork::V2,
|
||||||
|
@ -128,8 +129,8 @@ impl HardFork {
|
||||||
/// Returns the hard-fork for a blocks `minor_version` (vote) field.
|
/// Returns the hard-fork for a blocks `minor_version` (vote) field.
|
||||||
///
|
///
|
||||||
/// https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#blocks-version-and-vote
|
/// https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#blocks-version-and-vote
|
||||||
pub fn from_vote(vote: &u8) -> HardFork {
|
pub fn from_vote(vote: u8) -> HardFork {
|
||||||
if *vote == 0 {
|
if vote == 0 {
|
||||||
// A vote of 0 is interpreted as 1 as that's what Monero used to default to.
|
// A vote of 0 is interpreted as 1 as that's what Monero used to default to.
|
||||||
return HardFork::V1;
|
return HardFork::V1;
|
||||||
}
|
}
|
||||||
|
@ -137,9 +138,16 @@ impl HardFork {
|
||||||
Self::from_version(vote).unwrap_or(HardFork::V16)
|
Self::from_version(vote).unwrap_or(HardFork::V16)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_block_header(header: &BlockHeader) -> Result<(HardFork, HardFork), HardForkError> {
|
||||||
|
Ok((
|
||||||
|
HardFork::from_version(header.major_version)?,
|
||||||
|
HardFork::from_vote(header.minor_version),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the next hard-fork.
|
/// Returns the next hard-fork.
|
||||||
pub fn next_fork(&self) -> Option<HardFork> {
|
pub fn next_fork(&self) -> Option<HardFork> {
|
||||||
HardFork::from_version(&(*self as u8 + 1)).ok()
|
HardFork::from_version(*self as u8 + 1).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the target block time for this hardfork.
|
/// Returns the target block time for this hardfork.
|
||||||
|
@ -154,14 +162,15 @@ impl HardFork {
|
||||||
///
|
///
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#version-and-vote
|
/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#version-and-vote
|
||||||
pub fn check_block_version_vote(
|
pub fn check_block_version_vote(
|
||||||
current_hf: &Self,
|
&self,
|
||||||
version: &HardFork,
|
version: &HardFork,
|
||||||
vote: &HardFork,
|
vote: &HardFork,
|
||||||
) -> Result<(), HardForkError> {
|
) -> Result<(), HardForkError> {
|
||||||
if current_hf != version {
|
// self = current hf
|
||||||
|
if self != version {
|
||||||
Err(HardForkError::VersionIncorrect)?;
|
Err(HardForkError::VersionIncorrect)?;
|
||||||
}
|
}
|
||||||
if current_hf < vote {
|
if self < vote {
|
||||||
Err(HardForkError::VoteTooLow)?;
|
Err(HardForkError::VoteTooLow)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,7 +244,7 @@ impl HFVotes {
|
||||||
/// Checks if a future hard fork should be activated, returning the next hard-fork that should be
|
/// Checks if a future hard fork should be activated, returning the next hard-fork that should be
|
||||||
/// activated.
|
/// activated.
|
||||||
///
|
///
|
||||||
/// https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#accepting-a-fork
|
/// https://cuprate.github.io/monero-book/consensus_rules/hardforks.html#accepting-a-fork
|
||||||
pub fn check_next_hard_fork(
|
pub fn check_next_hard_fork(
|
||||||
&self,
|
&self,
|
||||||
current_hf: &HardFork,
|
current_hf: &HardFork,
|
||||||
|
|
|
@ -1,26 +1,25 @@
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
pub mod blocks;
|
||||||
mod decomposed_amount;
|
mod decomposed_amount;
|
||||||
|
pub mod genesis;
|
||||||
mod hard_forks;
|
mod hard_forks;
|
||||||
mod miner_tx;
|
pub mod miner_tx;
|
||||||
mod signatures;
|
pub mod signatures;
|
||||||
mod transactions;
|
pub mod transactions;
|
||||||
|
|
||||||
pub use decomposed_amount::is_decomposed_amount;
|
pub use decomposed_amount::is_decomposed_amount;
|
||||||
pub use hard_forks::{HFVotes, HFsInfo, HardFork};
|
pub use hard_forks::{HFVotes, HFsInfo, HardFork};
|
||||||
|
pub use transactions::TxVersion;
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||||
pub enum TxVersion {
|
pub enum ConsensusError {
|
||||||
RingSignatures,
|
#[error("Block error: {0}")]
|
||||||
RingCT,
|
Block(#[from] blocks::BlockError),
|
||||||
}
|
#[error("Transaction error: {0}")]
|
||||||
|
Transaction(#[from] transactions::TransactionError),
|
||||||
impl TxVersion {
|
#[error("Signatures error: {0}")]
|
||||||
pub fn from_raw(version: u64) -> Option<TxVersion> {
|
Signatures(#[from] signatures::SignatureError),
|
||||||
Some(match version {
|
|
||||||
1 => TxVersion::RingSignatures,
|
|
||||||
2 => TxVersion::RingCT,
|
|
||||||
_ => return None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks that a point is canonical.
|
/// Checks that a point is canonical.
|
||||||
|
@ -36,6 +35,13 @@ fn check_point(point: &curve25519_dalek::edwards::CompressedEdwardsY) -> bool {
|
||||||
.is_some()
|
.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn current_time() -> u64 {
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "rayon")]
|
#[cfg(feature = "rayon")]
|
||||||
fn try_par_iter<T>(t: T) -> T::Iter
|
fn try_par_iter<T>(t: T) -> T::Iter
|
||||||
where
|
where
|
||||||
|
|
|
@ -12,7 +12,7 @@ use monero_serai::{ring_signatures::RingSignature, transaction::Input};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
|
|
||||||
use super::{Rings, SignatureError};
|
use super::{Rings, SignatureError};
|
||||||
use crate::par_iter;
|
use crate::try_par_iter;
|
||||||
|
|
||||||
/// Verifies the ring signature.
|
/// Verifies the ring signature.
|
||||||
///
|
///
|
||||||
|
@ -31,7 +31,7 @@ pub fn verify_inputs_signatures(
|
||||||
return Err(SignatureError::MismatchSignatureSize);
|
return Err(SignatureError::MismatchSignatureSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
par_iter(inputs)
|
try_par_iter(inputs)
|
||||||
.zip(rings)
|
.zip(rings)
|
||||||
.zip(signatures)
|
.zip(signatures)
|
||||||
.try_for_each(|((input, ring), sig)| {
|
.try_for_each(|((input, ring), sig)| {
|
||||||
|
|
|
@ -2,13 +2,15 @@ use std::{cmp::Ordering, collections::HashSet, sync::Arc};
|
||||||
|
|
||||||
use monero_serai::transaction::{Input, Output, Timelock};
|
use monero_serai::transaction::{Input, Output, Timelock};
|
||||||
|
|
||||||
use crate::{check_point, is_decomposed_amount, HardFork, TxVersion};
|
use crate::{check_point, is_decomposed_amount, HardFork};
|
||||||
|
|
||||||
mod contextual_data;
|
mod contextual_data;
|
||||||
pub use contextual_data::*;
|
pub use contextual_data::*;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||||
pub enum TransactionError {
|
pub enum TransactionError {
|
||||||
|
#[error("The transactions version is incorrect.")]
|
||||||
|
TransactionVersionInvalid,
|
||||||
//-------------------------------------------------------- OUTPUTS
|
//-------------------------------------------------------- OUTPUTS
|
||||||
#[error("Output is not a valid point.")]
|
#[error("Output is not a valid point.")]
|
||||||
OutputNotValidPoint,
|
OutputNotValidPoint,
|
||||||
|
@ -20,6 +22,8 @@ pub enum TransactionError {
|
||||||
AmountNotDecomposed,
|
AmountNotDecomposed,
|
||||||
#[error("The transactions outputs overflow.")]
|
#[error("The transactions outputs overflow.")]
|
||||||
OutputsOverflow,
|
OutputsOverflow,
|
||||||
|
#[error("The transactions outputs too much.")]
|
||||||
|
OutputsTooHigh,
|
||||||
//-------------------------------------------------------- INPUTS
|
//-------------------------------------------------------- INPUTS
|
||||||
#[error("One or more inputs don't have the expected number of decoys.")]
|
#[error("One or more inputs don't have the expected number of decoys.")]
|
||||||
InputDoesNotHaveExpectedNumbDecoys,
|
InputDoesNotHaveExpectedNumbDecoys,
|
||||||
|
@ -45,6 +49,22 @@ pub enum TransactionError {
|
||||||
RingMemberNotFound,
|
RingMemberNotFound,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
|
pub enum TxVersion {
|
||||||
|
RingSignatures,
|
||||||
|
RingCT,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TxVersion {
|
||||||
|
pub fn from_raw(version: u64) -> Option<TxVersion> {
|
||||||
|
Some(match version {
|
||||||
|
1 => TxVersion::RingSignatures,
|
||||||
|
2 => TxVersion::RingCT,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//----------------------------------------------------------------------------------------------------------- OUTPUTS
|
//----------------------------------------------------------------------------------------------------------- OUTPUTS
|
||||||
|
|
||||||
/// Checks the output keys are canonical points.
|
/// Checks the output keys are canonical points.
|
||||||
|
@ -426,3 +446,48 @@ pub fn check_inputs(
|
||||||
_ => panic!("TODO: RCT"),
|
_ => panic!("TODO: RCT"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks the version is in the allowed range.
|
||||||
|
///
|
||||||
|
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#version
|
||||||
|
pub fn check_tx_version(
|
||||||
|
decoy_info: &Option<contextual_data::DecoyInfo>,
|
||||||
|
version: &TxVersion,
|
||||||
|
hf: &HardFork,
|
||||||
|
) -> Result<(), TransactionError> {
|
||||||
|
if let Some(decoy_info) = decoy_info {
|
||||||
|
let max = max_tx_version(hf);
|
||||||
|
if version > &max {
|
||||||
|
return Err(TransactionError::TransactionVersionInvalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Doc is wrong here
|
||||||
|
let min = min_tx_version(hf);
|
||||||
|
if version < &min && decoy_info.not_mixable != 0 {
|
||||||
|
return Err(TransactionError::TransactionVersionInvalid);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This will only happen for hard-fork 1 when only RingSignatures are allowed.
|
||||||
|
if version != &TxVersion::RingSignatures {
|
||||||
|
return Err(TransactionError::TransactionVersionInvalid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn max_tx_version(hf: &HardFork) -> TxVersion {
|
||||||
|
if hf <= &HardFork::V3 {
|
||||||
|
TxVersion::RingSignatures
|
||||||
|
} else {
|
||||||
|
TxVersion::RingCT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn min_tx_version(hf: &HardFork) -> TxVersion {
|
||||||
|
if hf >= &HardFork::V6 {
|
||||||
|
TxVersion::RingCT
|
||||||
|
} else {
|
||||||
|
TxVersion::RingSignatures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -11,10 +11,10 @@ use crate::{transactions::TransactionError, HardFork, TxVersion};
|
||||||
/// An already approved previous transaction output.
|
/// An already approved previous transaction output.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct OutputOnChain {
|
pub struct OutputOnChain {
|
||||||
height: u64,
|
pub height: u64,
|
||||||
time_lock: Timelock,
|
pub time_lock: Timelock,
|
||||||
key: EdwardsPoint,
|
pub key: EdwardsPoint,
|
||||||
mask: EdwardsPoint,
|
pub mask: EdwardsPoint,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the absolute offsets from the relative offsets.
|
/// Gets the absolute offsets from the relative offsets.
|
||||||
|
@ -67,7 +67,7 @@ pub fn insert_ring_member_ids(
|
||||||
/// Get the ring members for the inputs from the outputs on the chain.
|
/// Get the ring members for the inputs from the outputs on the chain.
|
||||||
///
|
///
|
||||||
/// Will error if `outputs` does not contain the outputs needed.
|
/// Will error if `outputs` does not contain the outputs needed.
|
||||||
fn get_ring_members_for_inputs<'a>(
|
pub fn get_ring_members_for_inputs<'a>(
|
||||||
outputs: &'a HashMap<u64, HashMap<u64, OutputOnChain>>,
|
outputs: &'a HashMap<u64, HashMap<u64, OutputOnChain>>,
|
||||||
inputs: &[Input],
|
inputs: &[Input],
|
||||||
) -> Result<Vec<Vec<&'a OutputOnChain>>, TransactionError> {
|
) -> Result<Vec<Vec<&'a OutputOnChain>>, TransactionError> {
|
||||||
|
@ -139,16 +139,18 @@ pub struct TxRingMembersInfo {
|
||||||
pub decoy_info: Option<DecoyInfo>,
|
pub decoy_info: Option<DecoyInfo>,
|
||||||
pub youngest_used_out_height: u64,
|
pub youngest_used_out_height: u64,
|
||||||
pub time_locked_outs: Vec<Timelock>,
|
pub time_locked_outs: Vec<Timelock>,
|
||||||
|
pub hf: HardFork,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TxRingMembersInfo {
|
impl TxRingMembersInfo {
|
||||||
/// Construct a [`TxRingMembersInfo`] struct.
|
/// Construct a [`TxRingMembersInfo`] struct.
|
||||||
///
|
///
|
||||||
/// The used outs must be all the ring members used in the transactions inputs.
|
/// The used outs must be all the ring members used in the transactions inputs.
|
||||||
fn new(
|
pub fn new(
|
||||||
used_outs: Vec<Vec<&OutputOnChain>>,
|
used_outs: Vec<Vec<&OutputOnChain>>,
|
||||||
decoy_info: Option<DecoyInfo>,
|
decoy_info: Option<DecoyInfo>,
|
||||||
tx_version: TxVersion,
|
tx_version: TxVersion,
|
||||||
|
hf: HardFork,
|
||||||
) -> TxRingMembersInfo {
|
) -> TxRingMembersInfo {
|
||||||
TxRingMembersInfo {
|
TxRingMembersInfo {
|
||||||
youngest_used_out_height: used_outs
|
youngest_used_out_height: used_outs
|
||||||
|
@ -175,6 +177,7 @@ impl TxRingMembersInfo {
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
hf,
|
||||||
rings: Rings::new(used_outs, tx_version),
|
rings: Rings::new(used_outs, tx_version),
|
||||||
decoy_info,
|
decoy_info,
|
||||||
}
|
}
|
||||||
|
@ -216,7 +219,7 @@ impl DecoyInfo {
|
||||||
///
|
///
|
||||||
pub fn new(
|
pub fn new(
|
||||||
inputs: &[Input],
|
inputs: &[Input],
|
||||||
outputs_with_amount: &[Option<usize>],
|
outputs_with_amount: &HashMap<u64, usize>,
|
||||||
hf: &HardFork,
|
hf: &HardFork,
|
||||||
) -> Result<DecoyInfo, TransactionError> {
|
) -> Result<DecoyInfo, TransactionError> {
|
||||||
let mut min_decoys = usize::MAX;
|
let mut min_decoys = usize::MAX;
|
||||||
|
@ -226,10 +229,18 @@ impl DecoyInfo {
|
||||||
|
|
||||||
let minimum_decoys = minimum_decoys(hf);
|
let minimum_decoys = minimum_decoys(hf);
|
||||||
|
|
||||||
for (inp, outs_with_amt) in inputs.iter().zip(outputs_with_amount) {
|
for inp in inputs.iter() {
|
||||||
match inp {
|
match inp {
|
||||||
Input::ToKey { key_offsets, .. } => {
|
Input::ToKey {
|
||||||
if let Some(outs_with_amt) = *outs_with_amt {
|
key_offsets,
|
||||||
|
amount,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if let Some(amount) = amount {
|
||||||
|
let outs_with_amt = *outputs_with_amount
|
||||||
|
.get(amount)
|
||||||
|
.expect("outputs_with_amount does not include needed amount.");
|
||||||
|
|
||||||
// https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html#mixable-and-unmixable-inputs
|
// https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html#mixable-and-unmixable-inputs
|
||||||
if outs_with_amt <= minimum_decoys {
|
if outs_with_amt <= minimum_decoys {
|
||||||
not_mixable += 1;
|
not_mixable += 1;
|
||||||
|
|
|
@ -14,7 +14,7 @@ use tracing::level_filters::LevelFilter;
|
||||||
|
|
||||||
use cuprate_common::Network;
|
use cuprate_common::Network;
|
||||||
|
|
||||||
use monero_consensus::{
|
use cuprate_consensus::{
|
||||||
context::{
|
context::{
|
||||||
BlockChainContextRequest, BlockChainContextResponse, ContextConfig,
|
BlockChainContextRequest, BlockChainContextResponse, ContextConfig,
|
||||||
UpdateBlockchainCacheData,
|
UpdateBlockchainCacheData,
|
||||||
|
@ -170,42 +170,13 @@ where
|
||||||
call_blocks(new_tx_chan, block_tx, start_height, chain_height, database).await
|
call_blocks(new_tx_chan, block_tx, start_height, chain_height, database).await
|
||||||
});
|
});
|
||||||
|
|
||||||
let (mut prepared_blocks_tx, mut prepared_blocks_rx) = mpsc::channel(3);
|
while let Some(incoming_blocks) = incoming_blocks.next().await {
|
||||||
|
|
||||||
let mut cloned_block_verifier = block_verifier.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
while let Some(mut next_blocks) = incoming_blocks.next().await {
|
|
||||||
while !next_blocks.is_empty() {
|
|
||||||
tracing::info!(
|
|
||||||
"preparing next batch, number of blocks: {}",
|
|
||||||
next_blocks.len().min(150)
|
|
||||||
);
|
|
||||||
|
|
||||||
let res = cloned_block_verifier
|
|
||||||
.ready()
|
|
||||||
.await?
|
|
||||||
.call(VerifyBlockRequest::BatchSetup(
|
|
||||||
next_blocks.drain(0..next_blocks.len().min(150)).collect(),
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
prepared_blocks_tx.send(res).await.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Result::<_, tower::BoxError>::Ok(())
|
|
||||||
});
|
|
||||||
|
|
||||||
while let Some(prepared_blocks) = prepared_blocks_rx.next().await {
|
|
||||||
let VerifyBlockResponse::BatchSetup(prepared_blocks) = prepared_blocks? else {
|
|
||||||
panic!("block verifier sent incorrect response!");
|
|
||||||
};
|
|
||||||
let mut height = 0;
|
let mut height = 0;
|
||||||
for block in prepared_blocks {
|
for block in incoming_blocks {
|
||||||
let VerifyBlockResponse::MainChain(verified_block_info) = block_verifier
|
let VerifyBlockResponse::MainChain(verified_block_info) = block_verifier
|
||||||
.ready()
|
.ready()
|
||||||
.await?
|
.await?
|
||||||
.call(VerifyBlockRequest::MainChainPreparedBlock(block))
|
.call(VerifyBlockRequest::MainChain(block))
|
||||||
.await?
|
.await?
|
||||||
else {
|
else {
|
||||||
panic!("Block verifier sent incorrect response!");
|
panic!("Block verifier sent incorrect response!");
|
||||||
|
@ -219,13 +190,15 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
update_cache_and_context(&cache, &mut ctx_svc, verified_block_info).await?;
|
update_cache_and_context(&cache, &mut ctx_svc, verified_block_info).await?;
|
||||||
}
|
|
||||||
|
|
||||||
tracing::info!(
|
if height % 200 == 0 {
|
||||||
"verified blocks: {:?}, chain height: {}",
|
tracing::info!(
|
||||||
0..height,
|
"verified blocks: {:?}, chain height: {}",
|
||||||
chain_height
|
0..height,
|
||||||
);
|
chain_height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -15,13 +15,13 @@ use tower::{Service, ServiceExt};
|
||||||
|
|
||||||
use cuprate_common::tower_utils::InfallibleOneshotReceiver;
|
use cuprate_common::tower_utils::InfallibleOneshotReceiver;
|
||||||
|
|
||||||
use monero_consensus::{
|
use cuprate_consensus::{
|
||||||
context::{
|
context::{
|
||||||
BlockChainContext, BlockChainContextRequest, BlockChainContextResponse,
|
BlockChainContext, BlockChainContextRequest, BlockChainContextResponse,
|
||||||
RawBlockChainContext,
|
RawBlockChainContext,
|
||||||
},
|
},
|
||||||
transactions::{TransactionVerificationData, VerifyTxRequest, VerifyTxResponse},
|
transactions::{TransactionVerificationData, VerifyTxRequest, VerifyTxResponse},
|
||||||
ConsensusError, TxNotInPool, TxPoolRequest, TxPoolResponse,
|
ExtendedConsensusError, TxNotInPool, TxPoolRequest, TxPoolResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -78,7 +78,7 @@ pub struct TxPool<TxV, Ctx> {
|
||||||
|
|
||||||
impl<TxV, Ctx> TxPool<TxV, Ctx>
|
impl<TxV, Ctx> TxPool<TxV, Ctx>
|
||||||
where
|
where
|
||||||
TxV: Service<VerifyTxRequest, Response = VerifyTxResponse, Error = ConsensusError>
|
TxV: Service<VerifyTxRequest, Response = VerifyTxResponse, Error = ExtendedConsensusError>
|
||||||
+ Clone
|
+ Clone
|
||||||
+ Send
|
+ Send
|
||||||
+ 'static,
|
+ 'static,
|
||||||
|
@ -199,7 +199,7 @@ where
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.call(VerifyTxRequest::BatchSetup {
|
.call(VerifyTxRequest::BatchSetup {
|
||||||
txs: new_txs,
|
txs: new_txs,
|
||||||
hf: current_ctx.current_hard_fork,
|
hf: current_ctx.current_hf,
|
||||||
re_org_token: current_ctx.re_org_token.clone(),
|
re_org_token: current_ctx.re_org_token.clone(),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -7,22 +7,20 @@ use std::{
|
||||||
|
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use monero_serai::{block::Block, transaction::Input};
|
use monero_serai::{block::Block, transaction::Input};
|
||||||
use rayon::prelude::*;
|
|
||||||
use tower::{Service, ServiceExt};
|
use tower::{Service, ServiceExt};
|
||||||
|
|
||||||
|
use monero_consensus::{
|
||||||
|
blocks::{calculate_pow_hash, check_block, check_block_pow},
|
||||||
|
ConsensusError, HardFork,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
context::{BlockChainContextRequest, BlockChainContextResponse},
|
context::{BlockChainContextRequest, BlockChainContextResponse},
|
||||||
helper::rayon_spawn_async,
|
helper::rayon_spawn_async,
|
||||||
transactions::{TransactionVerificationData, VerifyTxRequest, VerifyTxResponse},
|
transactions::{TransactionVerificationData, VerifyTxRequest, VerifyTxResponse},
|
||||||
ConsensusError, HardFork, TxNotInPool, TxPoolRequest, TxPoolResponse,
|
ExtendedConsensusError, TxNotInPool, TxPoolRequest, TxPoolResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod checks;
|
|
||||||
mod hash_worker;
|
|
||||||
mod miner_tx;
|
|
||||||
|
|
||||||
use hash_worker::calculate_pow_hash;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct PrePreparedBlock {
|
pub struct PrePreparedBlock {
|
||||||
pub block: Block,
|
pub block: Block,
|
||||||
|
@ -53,9 +51,6 @@ pub struct VerifiedBlockInformation {
|
||||||
|
|
||||||
pub enum VerifyBlockRequest {
|
pub enum VerifyBlockRequest {
|
||||||
MainChain(Block),
|
MainChain(Block),
|
||||||
|
|
||||||
BatchSetup(Vec<Block>),
|
|
||||||
MainChainPreparedBlock(PrePreparedBlock),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum VerifyBlockResponse {
|
pub enum VerifyBlockResponse {
|
||||||
|
@ -79,7 +74,7 @@ where
|
||||||
+ Clone
|
+ Clone
|
||||||
+ Send
|
+ Send
|
||||||
+ 'static,
|
+ 'static,
|
||||||
TxV: Service<VerifyTxRequest, Response = VerifyTxResponse, Error = ConsensusError>
|
TxV: Service<VerifyTxRequest, Response = VerifyTxResponse, Error = ExtendedConsensusError>
|
||||||
+ Clone
|
+ Clone
|
||||||
+ Send
|
+ Send
|
||||||
+ 'static,
|
+ 'static,
|
||||||
|
@ -112,7 +107,7 @@ where
|
||||||
+ 'static,
|
+ 'static,
|
||||||
C::Future: Send + 'static,
|
C::Future: Send + 'static,
|
||||||
|
|
||||||
TxV: Service<VerifyTxRequest, Response = VerifyTxResponse, Error = ConsensusError>
|
TxV: Service<VerifyTxRequest, Response = VerifyTxResponse, Error = ExtendedConsensusError>
|
||||||
+ Clone
|
+ Clone
|
||||||
+ Send
|
+ Send
|
||||||
+ 'static,
|
+ 'static,
|
||||||
|
@ -125,7 +120,7 @@ where
|
||||||
TxP::Future: Send + 'static,
|
TxP::Future: Send + 'static,
|
||||||
{
|
{
|
||||||
type Response = VerifyBlockResponse;
|
type Response = VerifyBlockResponse;
|
||||||
type Error = ConsensusError;
|
type Error = ExtendedConsensusError;
|
||||||
type Future =
|
type Future =
|
||||||
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
|
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
|
||||||
|
|
||||||
|
@ -143,158 +138,18 @@ where
|
||||||
VerifyBlockRequest::MainChain(block) => {
|
VerifyBlockRequest::MainChain(block) => {
|
||||||
verify_main_chain_block(block, context_svc, tx_verifier_svc, tx_pool).await
|
verify_main_chain_block(block, context_svc, tx_verifier_svc, tx_pool).await
|
||||||
}
|
}
|
||||||
VerifyBlockRequest::BatchSetup(blocks) => batch_prepare_block(blocks).await,
|
|
||||||
VerifyBlockRequest::MainChainPreparedBlock(block) => {
|
|
||||||
verify_prepared_main_chain_block(block, context_svc, tx_verifier_svc, tx_pool)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn batch_prepare_block(blocks: Vec<Block>) -> Result<VerifyBlockResponse, ConsensusError> {
|
|
||||||
Ok(VerifyBlockResponse::BatchSetup(
|
|
||||||
rayon_spawn_async(move || {
|
|
||||||
blocks
|
|
||||||
.into_par_iter()
|
|
||||||
.map(prepare_block)
|
|
||||||
.collect::<Result<Vec<_>, _>>()
|
|
||||||
})
|
|
||||||
.await?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_block(block: Block) -> Result<PrePreparedBlock, ConsensusError> {
|
|
||||||
let hf_version = HardFork::from_version(&block.header.major_version)?;
|
|
||||||
let hf_vote = HardFork::from_vote(&block.header.major_version);
|
|
||||||
|
|
||||||
let height = match block.miner_tx.prefix.inputs.get(0) {
|
|
||||||
Some(Input::Gen(height)) => *height,
|
|
||||||
_ => {
|
|
||||||
return Err(ConsensusError::MinerTransaction(
|
|
||||||
"Input is not a miner input",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::debug!("preparing block: {}", height);
|
|
||||||
|
|
||||||
Ok(PrePreparedBlock {
|
|
||||||
block_blob: block.serialize(),
|
|
||||||
block_hash: block.hash(),
|
|
||||||
pow_hash: calculate_pow_hash(&block.serialize_hashable(), height, &hf_version)?,
|
|
||||||
miner_tx_weight: block.miner_tx.weight(),
|
|
||||||
block,
|
|
||||||
hf_vote,
|
|
||||||
hf_version,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn verify_prepared_main_chain_block<C, TxV, TxP>(
|
|
||||||
block: PrePreparedBlock,
|
|
||||||
context_svc: C,
|
|
||||||
tx_verifier_svc: TxV,
|
|
||||||
tx_pool: TxP,
|
|
||||||
) -> Result<VerifyBlockResponse, ConsensusError>
|
|
||||||
where
|
|
||||||
C: Service<
|
|
||||||
BlockChainContextRequest,
|
|
||||||
Response = BlockChainContextResponse,
|
|
||||||
Error = tower::BoxError,
|
|
||||||
> + Send
|
|
||||||
+ 'static,
|
|
||||||
C::Future: Send + 'static,
|
|
||||||
TxV: Service<VerifyTxRequest, Response = VerifyTxResponse, Error = ConsensusError>,
|
|
||||||
TxP: Service<TxPoolRequest, Response = TxPoolResponse, Error = TxNotInPool>
|
|
||||||
+ Clone
|
|
||||||
+ Send
|
|
||||||
+ 'static,
|
|
||||||
{
|
|
||||||
tracing::debug!("getting blockchain context");
|
|
||||||
let BlockChainContextResponse::Context(checked_context) = context_svc
|
|
||||||
.oneshot(BlockChainContextRequest::Get)
|
|
||||||
.await
|
|
||||||
.map_err(Into::<ConsensusError>::into)?
|
|
||||||
else {
|
|
||||||
panic!("Context service returned wrong response!");
|
|
||||||
};
|
|
||||||
|
|
||||||
let context = checked_context.unchecked_blockchain_context().clone();
|
|
||||||
|
|
||||||
tracing::debug!("got blockchain context: {:?}", context);
|
|
||||||
|
|
||||||
let txs = if !block.block.txs.is_empty() {
|
|
||||||
let TxPoolResponse::Transactions(txs) = tx_pool
|
|
||||||
.oneshot(TxPoolRequest::Transactions(block.block.txs.clone()))
|
|
||||||
.await?;
|
|
||||||
txs
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
};
|
|
||||||
|
|
||||||
let block_weight = block.miner_tx_weight + txs.iter().map(|tx| tx.tx_weight).sum::<usize>();
|
|
||||||
let total_fees = txs.iter().map(|tx| tx.fee).sum::<u64>();
|
|
||||||
|
|
||||||
if !txs.is_empty() {
|
|
||||||
tx_verifier_svc
|
|
||||||
.oneshot(VerifyTxRequest::Block {
|
|
||||||
txs: txs.clone(),
|
|
||||||
current_chain_height: context.chain_height,
|
|
||||||
time_for_time_lock: context.current_adjusted_timestamp_for_time_lock(),
|
|
||||||
hf: context.current_hard_fork,
|
|
||||||
re_org_token: context.re_org_token.clone(),
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let generated_coins = miner_tx::check_miner_tx(
|
|
||||||
&block.block.miner_tx,
|
|
||||||
total_fees,
|
|
||||||
context.chain_height,
|
|
||||||
block_weight,
|
|
||||||
context.median_weight_for_block_reward,
|
|
||||||
context.already_generated_coins,
|
|
||||||
&context.current_hard_fork,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
checks::block_size_sanity_check(block.block_blob.len(), context.effective_median_weight)?;
|
|
||||||
checks::block_weight_check(block_weight, context.median_weight_for_block_reward)?;
|
|
||||||
|
|
||||||
checks::check_amount_txs(block.block.txs.len())?;
|
|
||||||
checks::check_prev_id(&block.block, &context.top_hash)?;
|
|
||||||
if let Some(median_timestamp) = context.median_block_timestamp {
|
|
||||||
// will only be None for the first 60 blocks
|
|
||||||
checks::check_timestamp(&block.block, median_timestamp)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
checks::check_block_pow(&block.pow_hash, context.next_difficulty)?;
|
|
||||||
|
|
||||||
context
|
|
||||||
.current_hard_fork
|
|
||||||
.check_block_version_vote(&block.block.header)?;
|
|
||||||
|
|
||||||
Ok(VerifyBlockResponse::MainChain(VerifiedBlockInformation {
|
|
||||||
block_hash: block.block_hash,
|
|
||||||
block: block.block,
|
|
||||||
txs,
|
|
||||||
pow_hash: block.pow_hash,
|
|
||||||
generated_coins,
|
|
||||||
weight: block_weight,
|
|
||||||
height: context.chain_height,
|
|
||||||
long_term_weight: context.next_block_long_term_weight(block_weight),
|
|
||||||
hf_vote: block.hf_vote,
|
|
||||||
cumulative_difficulty: context.cumulative_difficulty + context.next_difficulty,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn verify_main_chain_block<C, TxV, TxP>(
|
async fn verify_main_chain_block<C, TxV, TxP>(
|
||||||
block: Block,
|
block: Block,
|
||||||
context_svc: C,
|
context_svc: C,
|
||||||
tx_verifier_svc: TxV,
|
tx_verifier_svc: TxV,
|
||||||
tx_pool: TxP,
|
tx_pool: TxP,
|
||||||
) -> Result<VerifyBlockResponse, ConsensusError>
|
) -> Result<VerifyBlockResponse, ExtendedConsensusError>
|
||||||
where
|
where
|
||||||
C: Service<
|
C: Service<
|
||||||
BlockChainContextRequest,
|
BlockChainContextRequest,
|
||||||
|
@ -303,7 +158,7 @@ where
|
||||||
> + Send
|
> + Send
|
||||||
+ 'static,
|
+ 'static,
|
||||||
C::Future: Send + 'static,
|
C::Future: Send + 'static,
|
||||||
TxV: Service<VerifyTxRequest, Response = VerifyTxResponse, Error = ConsensusError>,
|
TxV: Service<VerifyTxRequest, Response = VerifyTxResponse, Error = ExtendedConsensusError>,
|
||||||
TxP: Service<TxPoolRequest, Response = TxPoolResponse, Error = TxNotInPool>
|
TxP: Service<TxPoolRequest, Response = TxPoolResponse, Error = TxNotInPool>
|
||||||
+ Clone
|
+ Clone
|
||||||
+ Send
|
+ Send
|
||||||
|
@ -313,7 +168,7 @@ where
|
||||||
let BlockChainContextResponse::Context(checked_context) = context_svc
|
let BlockChainContextResponse::Context(checked_context) = context_svc
|
||||||
.oneshot(BlockChainContextRequest::Get)
|
.oneshot(BlockChainContextRequest::Get)
|
||||||
.await
|
.await
|
||||||
.map_err(Into::<ConsensusError>::into)?
|
.map_err(Into::<ExtendedConsensusError>::into)?
|
||||||
else {
|
else {
|
||||||
panic!("Context service returned wrong response!");
|
panic!("Context service returned wrong response!");
|
||||||
};
|
};
|
||||||
|
@ -326,57 +181,39 @@ where
|
||||||
.oneshot(TxPoolRequest::Transactions(block.txs.clone()))
|
.oneshot(TxPoolRequest::Transactions(block.txs.clone()))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let block_weight = block.miner_tx.weight() + txs.iter().map(|tx| tx.tx_weight).sum::<usize>();
|
|
||||||
let total_fees = txs.iter().map(|tx| tx.fee).sum::<u64>();
|
|
||||||
|
|
||||||
tx_verifier_svc
|
tx_verifier_svc
|
||||||
.oneshot(VerifyTxRequest::Block {
|
.oneshot(VerifyTxRequest::Block {
|
||||||
txs: txs.clone(),
|
txs: txs.clone(),
|
||||||
current_chain_height: context.chain_height,
|
current_chain_height: context.chain_height,
|
||||||
time_for_time_lock: context.current_adjusted_timestamp_for_time_lock(),
|
time_for_time_lock: context.current_adjusted_timestamp_for_time_lock(),
|
||||||
hf: context.current_hard_fork,
|
hf: context.current_hf,
|
||||||
re_org_token: context.re_org_token.clone(),
|
re_org_token: context.re_org_token.clone(),
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let generated_coins = miner_tx::check_miner_tx(
|
let block_weight = block.miner_tx.weight() + txs.iter().map(|tx| tx.tx_weight).sum::<usize>();
|
||||||
&block.miner_tx,
|
let total_fees = txs.iter().map(|tx| tx.fee).sum::<u64>();
|
||||||
|
|
||||||
|
let (hf_vote, generated_coins) = check_block(
|
||||||
|
&block,
|
||||||
total_fees,
|
total_fees,
|
||||||
context.chain_height,
|
|
||||||
block_weight,
|
block_weight,
|
||||||
context.median_weight_for_block_reward,
|
block.serialize().len(),
|
||||||
context.already_generated_coins,
|
&context.context_to_verify_block,
|
||||||
&context.current_hard_fork,
|
)
|
||||||
)?;
|
.map_err(ConsensusError::Block)?;
|
||||||
|
|
||||||
let hashing_blob = block.serialize_hashable();
|
let hashing_blob = block.serialize_hashable();
|
||||||
|
|
||||||
checks::block_size_sanity_check(block.serialize().len(), context.effective_median_weight)?;
|
|
||||||
checks::block_weight_check(block_weight, context.median_weight_for_block_reward)?;
|
|
||||||
|
|
||||||
checks::check_amount_txs(block.txs.len())?;
|
|
||||||
checks::check_prev_id(&block, &context.top_hash)?;
|
|
||||||
if let Some(median_timestamp) = context.median_block_timestamp {
|
|
||||||
// will only be None for the first 60 blocks
|
|
||||||
checks::check_timestamp(&block, median_timestamp)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// do POW test last
|
// do POW test last
|
||||||
let pow_hash = tokio::task::spawn_blocking(move || {
|
let chain_height = context.chain_height;
|
||||||
hash_worker::calculate_pow_hash(
|
let current_hf = context.current_hf;
|
||||||
&hashing_blob,
|
let pow_hash =
|
||||||
context.chain_height,
|
rayon_spawn_async(move || calculate_pow_hash(&hashing_blob, chain_height, ¤t_hf))
|
||||||
&context.current_hard_fork,
|
.await
|
||||||
)
|
.map_err(ConsensusError::Block)?;
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap()?;
|
|
||||||
|
|
||||||
checks::check_block_pow(&pow_hash, context.next_difficulty)?;
|
check_block_pow(&pow_hash, context.next_difficulty).map_err(ConsensusError::Block)?;
|
||||||
|
|
||||||
context
|
|
||||||
.current_hard_fork
|
|
||||||
.check_block_version_vote(&block.header)?;
|
|
||||||
|
|
||||||
Ok(VerifyBlockResponse::MainChain(VerifiedBlockInformation {
|
Ok(VerifyBlockResponse::MainChain(VerifiedBlockInformation {
|
||||||
block_hash: block.hash(),
|
block_hash: block.hash(),
|
||||||
|
@ -387,7 +224,7 @@ where
|
||||||
weight: block_weight,
|
weight: block_weight,
|
||||||
height: context.chain_height,
|
height: context.chain_height,
|
||||||
long_term_weight: context.next_block_long_term_weight(block_weight),
|
long_term_weight: context.next_block_long_term_weight(block_weight),
|
||||||
hf_vote: HardFork::V1,
|
hf_vote,
|
||||||
cumulative_difficulty: context.cumulative_difficulty + context.next_difficulty,
|
cumulative_difficulty: context.cumulative_difficulty + context.next_difficulty,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,85 +0,0 @@
|
||||||
use crypto_bigint::{CheckedMul, U256};
|
|
||||||
use monero_serai::block::Block;
|
|
||||||
|
|
||||||
use crate::{helper::current_time, ConsensusError};
|
|
||||||
|
|
||||||
const BLOCK_SIZE_SANITY_LEEWAY: usize = 100;
|
|
||||||
const BLOCK_FUTURE_TIME_LIMIT: u64 = 60 * 60 * 2;
|
|
||||||
|
|
||||||
/// Returns if the blocks POW hash is valid for the current difficulty.
|
|
||||||
///
|
|
||||||
/// See: https://cuprate.github.io/monero-book/consensus_rules/blocks/difficulty.html#checking-a-blocks-proof-of-work
|
|
||||||
pub fn check_block_pow(hash: &[u8; 32], difficulty: u128) -> Result<(), ConsensusError> {
|
|
||||||
let int_hash = U256::from_le_slice(hash);
|
|
||||||
|
|
||||||
let difficulty = U256::from_u128(difficulty);
|
|
||||||
|
|
||||||
if int_hash.checked_mul(&difficulty).is_some().unwrap_u8() != 1 {
|
|
||||||
Err(ConsensusError::BlockPOWInvalid)
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sanity check on the block blob size.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#block-weight-and-size
|
|
||||||
pub fn block_size_sanity_check(
|
|
||||||
block_blob_len: usize,
|
|
||||||
effective_median: usize,
|
|
||||||
) -> Result<(), ConsensusError> {
|
|
||||||
if block_blob_len > effective_median * 2 + BLOCK_SIZE_SANITY_LEEWAY {
|
|
||||||
Err(ConsensusError::BlockIsTooLarge)
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sanity check on number of txs in the block.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#amount-of-transactions
|
|
||||||
pub fn check_amount_txs(number_none_miner_txs: usize) -> Result<(), ConsensusError> {
|
|
||||||
if number_none_miner_txs + 1 > 0x10000000 {
|
|
||||||
Err(ConsensusError::BlockIsTooLarge)
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sanity check on the block weight.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#block-weight-and-siz
|
|
||||||
pub fn block_weight_check(
|
|
||||||
block_weight: usize,
|
|
||||||
median_for_block_reward: usize,
|
|
||||||
) -> Result<(), ConsensusError> {
|
|
||||||
if block_weight > median_for_block_reward * 2 {
|
|
||||||
Err(ConsensusError::BlockIsTooLarge)
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verifies the previous id is the last blocks hash
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#previous-id
|
|
||||||
pub fn check_prev_id(block: &Block, top_hash: &[u8; 32]) -> Result<(), ConsensusError> {
|
|
||||||
if &block.header.previous != top_hash {
|
|
||||||
Err(ConsensusError::BlockIsNotApartOfChain)
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks the blocks timestamp is in the valid range.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#timestamp
|
|
||||||
pub fn check_timestamp(block: &Block, median_timestamp: u64) -> Result<(), ConsensusError> {
|
|
||||||
if block.header.timestamp < median_timestamp
|
|
||||||
|| block.header.timestamp > current_time() + BLOCK_FUTURE_TIME_LIMIT
|
|
||||||
{
|
|
||||||
Err(ConsensusError::BlockTimestampInvalid)
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
use cryptonight_cuprate::{
|
|
||||||
cryptonight_hash_r, cryptonight_hash_v0, cryptonight_hash_v1, cryptonight_hash_v2,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{ConsensusError, HardFork};
|
|
||||||
|
|
||||||
/// Calcualtes the POW hash of this block.
|
|
||||||
pub fn calculate_pow_hash(
|
|
||||||
buf: &[u8],
|
|
||||||
height: u64,
|
|
||||||
hf: &HardFork,
|
|
||||||
) -> Result<[u8; 32], ConsensusError> {
|
|
||||||
if height == 202612 {
|
|
||||||
return Ok(
|
|
||||||
hex::decode("84f64766475d51837ac9efbef1926486e58563c95a19fef4aec3254f03000000")
|
|
||||||
.unwrap()
|
|
||||||
.try_into()
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(if hf.in_range(&HardFork::V1, &HardFork::V7) {
|
|
||||||
cryptonight_hash_v0(buf)
|
|
||||||
} else if hf == &HardFork::V7 {
|
|
||||||
cryptonight_hash_v1(buf).map_err(|_| ConsensusError::BlockPOWInvalid)?
|
|
||||||
} else if hf.in_range(&HardFork::V8, &HardFork::V10) {
|
|
||||||
cryptonight_hash_v2(buf)
|
|
||||||
} else if hf.in_range(&HardFork::V10, &HardFork::V12) {
|
|
||||||
cryptonight_hash_r(buf, height)
|
|
||||||
} else {
|
|
||||||
todo!("RandomX")
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,183 +0,0 @@
|
||||||
use monero_serai::ringct::RctType;
|
|
||||||
use monero_serai::transaction::{Input, Output, Timelock, Transaction};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
transactions::{
|
|
||||||
outputs::{check_output_types, is_decomposed_amount},
|
|
||||||
TxVersion,
|
|
||||||
},
|
|
||||||
ConsensusError, HardFork,
|
|
||||||
};
|
|
||||||
|
|
||||||
const MONEY_SUPPLY: u64 = u64::MAX;
|
|
||||||
const MINIMUM_REWARD_PER_MIN: u64 = 3 * 10_u64.pow(11);
|
|
||||||
|
|
||||||
const MINER_TX_TIME_LOCKED_BLOCKS: u64 = 60;
|
|
||||||
|
|
||||||
fn calculate_base_reward(already_generated_coins: u64, hf: &HardFork) -> u64 {
|
|
||||||
let target_mins = hf.block_time().as_secs() / 60;
|
|
||||||
let emission_speed_factor = 20 - (target_mins - 1);
|
|
||||||
((MONEY_SUPPLY - already_generated_coins) >> emission_speed_factor)
|
|
||||||
.max(MINIMUM_REWARD_PER_MIN * target_mins)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn calculate_block_reward(
|
|
||||||
block_weight: usize,
|
|
||||||
median_bw: usize,
|
|
||||||
already_generated_coins: u64,
|
|
||||||
hf: &HardFork,
|
|
||||||
) -> u64 {
|
|
||||||
let base_reward: u128 = calculate_base_reward(already_generated_coins, hf).into();
|
|
||||||
|
|
||||||
if block_weight <= median_bw {
|
|
||||||
return base_reward.try_into().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let multiplicand: u128 = ((2 * median_bw - block_weight) * block_weight)
|
|
||||||
.try_into()
|
|
||||||
.unwrap();
|
|
||||||
let effective_median_bw: u128 = median_bw.try_into().unwrap();
|
|
||||||
|
|
||||||
(((base_reward * multiplicand) / effective_median_bw) / effective_median_bw)
|
|
||||||
.try_into()
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks the miner transactions version.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#version
|
|
||||||
fn check_miner_tx_version(tx_version: &TxVersion, hf: &HardFork) -> Result<(), ConsensusError> {
|
|
||||||
// The TxVersion enum checks if the version is not 1 or 2
|
|
||||||
if hf >= &HardFork::V12 && tx_version != &TxVersion::RingCT {
|
|
||||||
Err(ConsensusError::MinerTransaction("Version invalid"))
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks the miner transactions inputs.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#input
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#height
|
|
||||||
fn check_inputs(inputs: &[Input], chain_height: u64) -> Result<(), ConsensusError> {
|
|
||||||
if inputs.len() != 1 {
|
|
||||||
return Err(ConsensusError::MinerTransaction(
|
|
||||||
"does not have exactly 1 input",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
match &inputs[0] {
|
|
||||||
Input::Gen(height) => {
|
|
||||||
if height != &chain_height {
|
|
||||||
Err(ConsensusError::MinerTransaction(
|
|
||||||
"Height in input is not expected height",
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Err(ConsensusError::MinerTransaction("Input not of type Gen")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks the miner transaction has a correct time lock.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#unlock-time
|
|
||||||
fn check_time_lock(time_lock: &Timelock, chain_height: u64) -> Result<(), ConsensusError> {
|
|
||||||
match time_lock {
|
|
||||||
Timelock::Block(till_height) => {
|
|
||||||
if u64::try_from(*till_height).unwrap() != chain_height + MINER_TX_TIME_LOCKED_BLOCKS {
|
|
||||||
tracing::warn!("{}, {}", till_height, chain_height);
|
|
||||||
Err(ConsensusError::MinerTransaction(
|
|
||||||
"Time lock has invalid block height",
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Err(ConsensusError::MinerTransaction(
|
|
||||||
"Time lock is not a block height",
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sums the outputs checking for overflow.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#output-amounts
|
|
||||||
fn sum_outputs(outputs: &[Output], hf: &HardFork) -> Result<u64, ConsensusError> {
|
|
||||||
let mut sum: u64 = 0;
|
|
||||||
for out in outputs {
|
|
||||||
let amt = out.amount.unwrap_or(0);
|
|
||||||
if hf == &HardFork::V3 && !is_decomposed_amount(amt) {
|
|
||||||
return Err(ConsensusError::MinerTransaction(
|
|
||||||
"output amount is not decomposed",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
sum = sum
|
|
||||||
.checked_add(amt)
|
|
||||||
.ok_or(ConsensusError::MinerTransaction(
|
|
||||||
"outputs overflow when summed",
|
|
||||||
))?;
|
|
||||||
}
|
|
||||||
Ok(sum)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks the total outputs amount is correct returning the amount of coins collected by the miner.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#total-outputs
|
|
||||||
fn check_total_output_amt(
|
|
||||||
total_output: u64,
|
|
||||||
reward: u64,
|
|
||||||
fees: u64,
|
|
||||||
hf: &HardFork,
|
|
||||||
) -> Result<u64, ConsensusError> {
|
|
||||||
if hf == &HardFork::V1 || hf >= &HardFork::V12 {
|
|
||||||
if total_output != reward + fees {
|
|
||||||
return Err(ConsensusError::MinerTransaction(
|
|
||||||
"miner transaction does not output correct amt",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(reward)
|
|
||||||
} else {
|
|
||||||
if total_output - fees > reward {
|
|
||||||
return Err(ConsensusError::MinerTransaction(
|
|
||||||
"miner transaction does not output correct amt",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if total_output > reward + fees {
|
|
||||||
return Err(ConsensusError::MinerTransaction(
|
|
||||||
"miner transaction does not output correct amt",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(total_output - fees)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn check_miner_tx(
|
|
||||||
tx: &Transaction,
|
|
||||||
total_fees: u64,
|
|
||||||
chain_height: u64,
|
|
||||||
block_weight: usize,
|
|
||||||
median_bw: usize,
|
|
||||||
already_generated_coins: u64,
|
|
||||||
hf: &HardFork,
|
|
||||||
) -> Result<u64, ConsensusError> {
|
|
||||||
let tx_version = TxVersion::from_raw(tx.prefix.version)?;
|
|
||||||
check_miner_tx_version(&tx_version, hf)?;
|
|
||||||
|
|
||||||
if hf >= &HardFork::V12 && tx.rct_signatures.rct_type() != RctType::Null {
|
|
||||||
return Err(ConsensusError::MinerTransaction("RctType is not null"));
|
|
||||||
}
|
|
||||||
|
|
||||||
check_time_lock(&tx.prefix.timelock, chain_height)?;
|
|
||||||
|
|
||||||
check_inputs(&tx.prefix.inputs, chain_height)?;
|
|
||||||
|
|
||||||
check_output_types(&tx.prefix.outputs, hf)?;
|
|
||||||
|
|
||||||
let reward = calculate_block_reward(block_weight, median_bw, already_generated_coins, hf);
|
|
||||||
let total_outs = sum_outputs(&tx.prefix.outputs, hf)?;
|
|
||||||
|
|
||||||
check_total_output_amt(total_outs, reward, total_fees, hf)
|
|
||||||
}
|
|
|
@ -20,8 +20,11 @@ use futures::{
|
||||||
use tower::{Service, ServiceExt};
|
use tower::{Service, ServiceExt};
|
||||||
|
|
||||||
use cuprate_common::tower_utils::InstaFuture;
|
use cuprate_common::tower_utils::InstaFuture;
|
||||||
|
use monero_consensus::{blocks::ContextToVerifyBlock, HardFork};
|
||||||
|
|
||||||
use crate::{helper::current_time, ConsensusError, Database, DatabaseRequest, DatabaseResponse};
|
use crate::{
|
||||||
|
helper::current_time, Database, DatabaseRequest, DatabaseResponse, ExtendedConsensusError,
|
||||||
|
};
|
||||||
|
|
||||||
mod difficulty;
|
mod difficulty;
|
||||||
mod hardforks;
|
mod hardforks;
|
||||||
|
@ -32,7 +35,7 @@ mod tests;
|
||||||
mod tokens;
|
mod tokens;
|
||||||
|
|
||||||
pub use difficulty::DifficultyCacheConfig;
|
pub use difficulty::DifficultyCacheConfig;
|
||||||
pub use hardforks::{HardFork, HardForkConfig};
|
pub use hardforks::HardForkConfig;
|
||||||
pub use tokens::*;
|
pub use tokens::*;
|
||||||
pub use weight::BlockWeightsCacheConfig;
|
pub use weight::BlockWeightsCacheConfig;
|
||||||
|
|
||||||
|
@ -69,7 +72,7 @@ pub async fn initialize_blockchain_context<D>(
|
||||||
+ Send
|
+ Send
|
||||||
+ Sync
|
+ Sync
|
||||||
+ 'static,
|
+ 'static,
|
||||||
ConsensusError,
|
ExtendedConsensusError,
|
||||||
>
|
>
|
||||||
where
|
where
|
||||||
D: Database + Clone + Send + Sync + 'static,
|
D: Database + Clone + Send + Sync + 'static,
|
||||||
|
@ -140,30 +143,21 @@ where
|
||||||
/// around. You should keep around [`BlockChainContext`] instead.
|
/// around. You should keep around [`BlockChainContext`] instead.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RawBlockChainContext {
|
pub struct RawBlockChainContext {
|
||||||
/// The next blocks difficulty.
|
|
||||||
pub next_difficulty: u128,
|
|
||||||
/// The current cumulative difficulty.
|
/// The current cumulative difficulty.
|
||||||
pub cumulative_difficulty: u128,
|
pub cumulative_difficulty: u128,
|
||||||
/// The current effective median block weight.
|
|
||||||
pub effective_median_weight: usize,
|
|
||||||
/// The median long term block weight.
|
|
||||||
median_long_term_weight: usize,
|
|
||||||
/// Median weight to use for block reward calculations.
|
|
||||||
pub median_weight_for_block_reward: usize,
|
|
||||||
/// The amount of coins minted already.
|
|
||||||
pub already_generated_coins: u64,
|
|
||||||
/// The median timestamp over the last [`BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW`] blocks, will be None if there aren't
|
|
||||||
/// [`BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW`] blocks.
|
|
||||||
pub median_block_timestamp: Option<u64>,
|
|
||||||
top_block_timestamp: Option<u64>,
|
|
||||||
/// The height of the chain.
|
|
||||||
pub chain_height: u64,
|
|
||||||
/// The top blocks hash
|
|
||||||
pub top_hash: [u8; 32],
|
|
||||||
/// The current hard fork.
|
|
||||||
pub current_hard_fork: HardFork,
|
|
||||||
/// A token which is used to signal if a reorg has happened since creating the token.
|
/// A token which is used to signal if a reorg has happened since creating the token.
|
||||||
pub re_org_token: ReOrgToken,
|
pub re_org_token: ReOrgToken,
|
||||||
|
pub context_to_verify_block: ContextToVerifyBlock,
|
||||||
|
/// The median long term block weight.
|
||||||
|
median_long_term_weight: usize,
|
||||||
|
top_block_timestamp: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for RawBlockChainContext {
|
||||||
|
type Target = ContextToVerifyBlock;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.context_to_verify_block
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RawBlockChainContext {
|
impl RawBlockChainContext {
|
||||||
|
@ -171,20 +165,19 @@ impl RawBlockChainContext {
|
||||||
///
|
///
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#getting-the-current-time
|
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#getting-the-current-time
|
||||||
pub fn current_adjusted_timestamp_for_time_lock(&self) -> u64 {
|
pub fn current_adjusted_timestamp_for_time_lock(&self) -> u64 {
|
||||||
if self.current_hard_fork < HardFork::V13 || self.median_block_timestamp.is_none() {
|
if self.current_hf < HardFork::V13 || self.median_block_timestamp.is_none() {
|
||||||
current_time()
|
current_time()
|
||||||
} else {
|
} else {
|
||||||
// This is safe as we just checked if this was None.
|
// This is safe as we just checked if this was None.
|
||||||
let median = self.median_block_timestamp.unwrap();
|
let median = self.median_block_timestamp.unwrap();
|
||||||
|
|
||||||
let adjusted_median = median
|
let adjusted_median = median
|
||||||
+ (BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW + 1)
|
+ (BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW + 1) * self.current_hf.block_time().as_secs()
|
||||||
* self.current_hard_fork.block_time().as_secs()
|
|
||||||
/ 2;
|
/ 2;
|
||||||
|
|
||||||
// This is safe as we just checked if the median was None and this will only be none for genesis and the first block.
|
// This is safe as we just checked if the median was None and this will only be none for genesis and the first block.
|
||||||
let adjusted_top_block =
|
let adjusted_top_block =
|
||||||
self.top_block_timestamp.unwrap() + self.current_hard_fork.block_time().as_secs();
|
self.top_block_timestamp.unwrap() + self.current_hf.block_time().as_secs();
|
||||||
|
|
||||||
min(adjusted_median, adjusted_top_block)
|
min(adjusted_median, adjusted_top_block)
|
||||||
}
|
}
|
||||||
|
@ -200,7 +193,7 @@ impl RawBlockChainContext {
|
||||||
|
|
||||||
pub fn next_block_long_term_weight(&self, block_weight: usize) -> usize {
|
pub fn next_block_long_term_weight(&self, block_weight: usize) -> usize {
|
||||||
weight::calculate_block_long_term_weight(
|
weight::calculate_block_long_term_weight(
|
||||||
&self.current_hard_fork,
|
&self.current_hf,
|
||||||
block_weight,
|
block_weight,
|
||||||
self.median_long_term_weight,
|
self.median_long_term_weight,
|
||||||
)
|
)
|
||||||
|
@ -346,21 +339,23 @@ impl Service<BlockChainContextRequest> for BlockChainContextService {
|
||||||
InstaFuture::from(Ok(BlockChainContextResponse::Context(BlockChainContext {
|
InstaFuture::from(Ok(BlockChainContextResponse::Context(BlockChainContext {
|
||||||
validity_token: current_validity_token.clone(),
|
validity_token: current_validity_token.clone(),
|
||||||
raw: RawBlockChainContext {
|
raw: RawBlockChainContext {
|
||||||
next_difficulty: difficulty_cache.next_difficulty(¤t_hf),
|
context_to_verify_block: ContextToVerifyBlock {
|
||||||
|
median_weight_for_block_reward: weight_cache
|
||||||
|
.median_for_block_reward(¤t_hf),
|
||||||
|
effective_median_weight: weight_cache
|
||||||
|
.effective_median_block_weight(¤t_hf),
|
||||||
|
top_hash: *top_block_hash,
|
||||||
|
median_block_timestamp: difficulty_cache.median_timestamp(
|
||||||
|
usize::try_from(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW).unwrap(),
|
||||||
|
),
|
||||||
|
chain_height: *chain_height,
|
||||||
|
current_hf,
|
||||||
|
next_difficulty: difficulty_cache.next_difficulty(¤t_hf),
|
||||||
|
already_generated_coins: *already_generated_coins,
|
||||||
|
},
|
||||||
cumulative_difficulty: difficulty_cache.cumulative_difficulty(),
|
cumulative_difficulty: difficulty_cache.cumulative_difficulty(),
|
||||||
effective_median_weight: weight_cache
|
|
||||||
.effective_median_block_weight(¤t_hf),
|
|
||||||
median_long_term_weight: weight_cache.median_long_term_weight(),
|
median_long_term_weight: weight_cache.median_long_term_weight(),
|
||||||
median_weight_for_block_reward: weight_cache
|
|
||||||
.median_for_block_reward(¤t_hf),
|
|
||||||
already_generated_coins: *already_generated_coins,
|
|
||||||
top_block_timestamp: difficulty_cache.top_block_timestamp(),
|
top_block_timestamp: difficulty_cache.top_block_timestamp(),
|
||||||
median_block_timestamp: difficulty_cache.median_timestamp(
|
|
||||||
usize::try_from(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW).unwrap(),
|
|
||||||
),
|
|
||||||
chain_height: *chain_height,
|
|
||||||
top_hash: *top_block_hash,
|
|
||||||
current_hard_fork: current_hf,
|
|
||||||
re_org_token: current_reorg_token.clone(),
|
re_org_token: current_reorg_token.clone(),
|
||||||
},
|
},
|
||||||
})))
|
})))
|
||||||
|
|
|
@ -4,7 +4,7 @@ use tower::ServiceExt;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
helper::median, ConsensusError, Database, DatabaseRequest, DatabaseResponse, HardFork,
|
helper::median, Database, DatabaseRequest, DatabaseResponse, ExtendedConsensusError, HardFork,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -74,7 +74,7 @@ impl DifficultyCache {
|
||||||
chain_height: u64,
|
chain_height: u64,
|
||||||
config: DifficultyCacheConfig,
|
config: DifficultyCacheConfig,
|
||||||
database: D,
|
database: D,
|
||||||
) -> Result<Self, ConsensusError> {
|
) -> Result<Self, ExtendedConsensusError> {
|
||||||
tracing::info!("Initializing difficulty cache this may take a while.");
|
tracing::info!("Initializing difficulty cache this may take a while.");
|
||||||
|
|
||||||
let mut block_start = chain_height.saturating_sub(config.total_block_count());
|
let mut block_start = chain_height.saturating_sub(config.total_block_count());
|
||||||
|
@ -212,7 +212,7 @@ fn get_window_start_and_end(
|
||||||
async fn get_blocks_in_pow_info<D: Database + Clone>(
|
async fn get_blocks_in_pow_info<D: Database + Clone>(
|
||||||
database: D,
|
database: D,
|
||||||
block_heights: Range<u64>,
|
block_heights: Range<u64>,
|
||||||
) -> Result<(VecDeque<u64>, VecDeque<u128>), ConsensusError> {
|
) -> Result<(VecDeque<u64>, VecDeque<u128>), ExtendedConsensusError> {
|
||||||
tracing::info!("Getting blocks timestamps");
|
tracing::info!("Getting blocks timestamps");
|
||||||
|
|
||||||
let DatabaseResponse::BlockExtendedHeaderInRange(ext_header) = database
|
let DatabaseResponse::BlockExtendedHeaderInRange(ext_header) = database
|
||||||
|
|
|
@ -1,260 +1,41 @@
|
||||||
use std::{
|
use std::ops::Range;
|
||||||
collections::VecDeque,
|
|
||||||
fmt::{Display, Formatter},
|
|
||||||
ops::Range,
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use monero_serai::block::BlockHeader;
|
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::{ConsensusError, Database, DatabaseRequest, DatabaseResponse};
|
use monero_consensus::{HFVotes, HFsInfo, HardFork};
|
||||||
|
|
||||||
|
use crate::{Database, DatabaseRequest, DatabaseResponse, ExtendedConsensusError};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(super) mod tests;
|
pub(super) mod tests;
|
||||||
|
|
||||||
// https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#accepting-a-fork
|
// https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#accepting-a-fork
|
||||||
const DEFAULT_WINDOW_SIZE: u64 = 10080; // supermajority window check length - a week
|
const DEFAULT_WINDOW_SIZE: u64 = 10080; // supermajority window check length - a week
|
||||||
const BLOCK_TIME_V1: Duration = Duration::from_secs(60);
|
|
||||||
const BLOCK_TIME_V2: Duration = Duration::from_secs(120);
|
|
||||||
|
|
||||||
const NUMB_OF_HARD_FORKS: usize = 16;
|
|
||||||
|
|
||||||
/// Information about a given hard-fork.
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct HFInfo {
|
|
||||||
height: u64,
|
|
||||||
threshold: u64,
|
|
||||||
}
|
|
||||||
impl HFInfo {
|
|
||||||
pub const fn new(height: u64, threshold: u64) -> HFInfo {
|
|
||||||
HFInfo { height, threshold }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the main-net hard-fork information.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/hardforks.html#Mainnet-Hard-Forks
|
|
||||||
pub const fn main_net() -> [HFInfo; NUMB_OF_HARD_FORKS] {
|
|
||||||
[
|
|
||||||
HFInfo::new(0, 0),
|
|
||||||
HFInfo::new(1009827, 0),
|
|
||||||
HFInfo::new(1141317, 0),
|
|
||||||
HFInfo::new(1220516, 0),
|
|
||||||
HFInfo::new(1288616, 0),
|
|
||||||
HFInfo::new(1400000, 0),
|
|
||||||
HFInfo::new(1546000, 0),
|
|
||||||
HFInfo::new(1685555, 0),
|
|
||||||
HFInfo::new(1686275, 0),
|
|
||||||
HFInfo::new(1788000, 0),
|
|
||||||
HFInfo::new(1788720, 0),
|
|
||||||
HFInfo::new(1978433, 0),
|
|
||||||
HFInfo::new(2210000, 0),
|
|
||||||
HFInfo::new(2210720, 0),
|
|
||||||
HFInfo::new(2688888, 0),
|
|
||||||
HFInfo::new(2689608, 0),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuration for hard-forks.
|
/// Configuration for hard-forks.
|
||||||
///
|
///
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct HardForkConfig {
|
pub struct HardForkConfig {
|
||||||
/// The network we are on.
|
/// The network we are on.
|
||||||
forks: [HFInfo; NUMB_OF_HARD_FORKS],
|
info: HFsInfo,
|
||||||
/// The amount of votes we are taking into account to decide on a fork activation.
|
/// The amount of votes we are taking into account to decide on a fork activation.
|
||||||
window: u64,
|
window: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HardForkConfig {
|
impl HardForkConfig {
|
||||||
fn fork_info(&self, hf: &HardFork) -> HFInfo {
|
|
||||||
self.forks[*hf as usize - 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn main_net() -> HardForkConfig {
|
pub const fn main_net() -> HardForkConfig {
|
||||||
Self {
|
Self {
|
||||||
forks: HFInfo::main_net(),
|
info: HFsInfo::main_net(),
|
||||||
window: DEFAULT_WINDOW_SIZE,
|
window: DEFAULT_WINDOW_SIZE,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An identifier for every hard-fork Monero has had.
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
|
|
||||||
#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
|
|
||||||
#[repr(u8)]
|
|
||||||
pub enum HardFork {
|
|
||||||
V1 = 1,
|
|
||||||
V2,
|
|
||||||
V3,
|
|
||||||
V4,
|
|
||||||
V5,
|
|
||||||
V6,
|
|
||||||
V7,
|
|
||||||
V8,
|
|
||||||
V9,
|
|
||||||
V10,
|
|
||||||
V11,
|
|
||||||
V12,
|
|
||||||
V13,
|
|
||||||
V14,
|
|
||||||
V15,
|
|
||||||
// remember to update from_vote!
|
|
||||||
V16,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HardFork {
|
|
||||||
/// Returns the hard-fork for a blocks `major_version` field.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#blocks-version-and-vote
|
|
||||||
pub fn from_version(version: &u8) -> Result<HardFork, ConsensusError> {
|
|
||||||
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(ConsensusError::InvalidHardForkVersion(
|
|
||||||
"Version is not a known hard fork",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the hard-fork for a blocks `minor_version` (vote) field.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-docs/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> {
|
|
||||||
HardFork::from_version(&(*self as u8 + 1)).ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns if the hard-fork is in range:
|
|
||||||
///
|
|
||||||
/// start <= hf < end
|
|
||||||
pub fn in_range(&self, start: &HardFork, end: &HardFork) -> bool {
|
|
||||||
start <= self && self < end
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the target block time for this hardfork.
|
|
||||||
pub fn block_time(&self) -> Duration {
|
|
||||||
match self {
|
|
||||||
HardFork::V1 => BLOCK_TIME_V1,
|
|
||||||
_ => BLOCK_TIME_V2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks a blocks version and vote, assuming that `self` is the current hard-fork.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#version-and-vote
|
|
||||||
pub fn check_block_version_vote(
|
|
||||||
&self,
|
|
||||||
block_header: &BlockHeader,
|
|
||||||
) -> Result<(), ConsensusError> {
|
|
||||||
let version = HardFork::from_version(&block_header.major_version)?;
|
|
||||||
let vote = HardFork::from_vote(&block_header.minor_version);
|
|
||||||
|
|
||||||
if self == &version && &vote >= self {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(ConsensusError::InvalidHardForkVersion(
|
|
||||||
"Block version or vote incorrect",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A struct holding the current voting state of the blockchain.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct HFVotes {
|
|
||||||
votes: [u64; NUMB_OF_HARD_FORKS],
|
|
||||||
vote_list: VecDeque<HardFork>,
|
|
||||||
window_size: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for HFVotes {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.debug_struct("HFVotes")
|
|
||||||
.field("total", &self.total_votes())
|
|
||||||
.field("V1", &self.votes_for_hf(&HardFork::V1))
|
|
||||||
.field("V2", &self.votes_for_hf(&HardFork::V2))
|
|
||||||
.field("V3", &self.votes_for_hf(&HardFork::V3))
|
|
||||||
.field("V4", &self.votes_for_hf(&HardFork::V4))
|
|
||||||
.field("V5", &self.votes_for_hf(&HardFork::V5))
|
|
||||||
.field("V6", &self.votes_for_hf(&HardFork::V6))
|
|
||||||
.field("V7", &self.votes_for_hf(&HardFork::V7))
|
|
||||||
.field("V8", &self.votes_for_hf(&HardFork::V8))
|
|
||||||
.field("V9", &self.votes_for_hf(&HardFork::V9))
|
|
||||||
.field("V10", &self.votes_for_hf(&HardFork::V10))
|
|
||||||
.field("V11", &self.votes_for_hf(&HardFork::V11))
|
|
||||||
.field("V12", &self.votes_for_hf(&HardFork::V12))
|
|
||||||
.field("V13", &self.votes_for_hf(&HardFork::V13))
|
|
||||||
.field("V14", &self.votes_for_hf(&HardFork::V14))
|
|
||||||
.field("V15", &self.votes_for_hf(&HardFork::V15))
|
|
||||||
.field("V16", &self.votes_for_hf(&HardFork::V16))
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HFVotes {
|
|
||||||
pub fn new(window_size: usize) -> HFVotes {
|
|
||||||
HFVotes {
|
|
||||||
votes: [0; NUMB_OF_HARD_FORKS],
|
|
||||||
vote_list: VecDeque::with_capacity(window_size),
|
|
||||||
window_size,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a vote for a hard-fork, this function removes votes outside of the window.
|
|
||||||
pub fn add_vote_for_hf(&mut self, hf: &HardFork) {
|
|
||||||
self.vote_list.push_back(*hf);
|
|
||||||
self.votes[*hf as usize - 1] += 1;
|
|
||||||
if self.vote_list.len() > self.window_size {
|
|
||||||
let hf = self.vote_list.pop_front().unwrap();
|
|
||||||
self.votes[hf as usize - 1] -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the total votes for a hard-fork.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#accepting-a-fork
|
|
||||||
pub fn 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A struct that keeps track of the current hard-fork and current votes.
|
/// A struct that keeps track of the current hard-fork and current votes.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct HardForkState {
|
pub struct HardForkState {
|
||||||
current_hardfork: HardFork,
|
current_hardfork: HardFork,
|
||||||
next_hardfork: Option<HardFork>,
|
|
||||||
|
|
||||||
config: HardForkConfig,
|
config: HardForkConfig,
|
||||||
votes: HFVotes,
|
votes: HFVotes,
|
||||||
|
@ -268,7 +49,7 @@ impl HardForkState {
|
||||||
chain_height: u64,
|
chain_height: u64,
|
||||||
config: HardForkConfig,
|
config: HardForkConfig,
|
||||||
mut database: D,
|
mut database: D,
|
||||||
) -> Result<Self, ConsensusError> {
|
) -> Result<Self, ExtendedConsensusError> {
|
||||||
tracing::info!("Initializing hard-fork state this may take a while.");
|
tracing::info!("Initializing hard-fork state this may take a while.");
|
||||||
|
|
||||||
let block_start = chain_height.saturating_sub(config.window);
|
let block_start = chain_height.saturating_sub(config.window);
|
||||||
|
@ -297,12 +78,9 @@ impl HardForkState {
|
||||||
|
|
||||||
let current_hardfork = ext_header.version;
|
let current_hardfork = ext_header.version;
|
||||||
|
|
||||||
let next_hardfork = current_hardfork.next_fork();
|
|
||||||
|
|
||||||
let mut hfs = HardForkState {
|
let mut hfs = HardForkState {
|
||||||
config,
|
config,
|
||||||
current_hardfork,
|
current_hardfork,
|
||||||
next_hardfork,
|
|
||||||
votes,
|
votes,
|
||||||
last_height: chain_height - 1,
|
last_height: chain_height - 1,
|
||||||
};
|
};
|
||||||
|
@ -341,25 +119,16 @@ impl HardForkState {
|
||||||
///
|
///
|
||||||
/// https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#accepting-a-fork
|
/// https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#accepting-a-fork
|
||||||
fn check_set_new_hf(&mut self) {
|
fn check_set_new_hf(&mut self) {
|
||||||
while let Some(new_hf) = self.next_hardfork {
|
if let Some(next_fork) = self.votes.check_next_hard_fork(
|
||||||
let hf_info = self.config.fork_info(&new_hf);
|
&self.current_hardfork,
|
||||||
if self.last_height + 1 >= hf_info.height
|
self.last_height + 1,
|
||||||
&& self.votes.votes_for_hf(&new_hf)
|
self.config.window,
|
||||||
>= votes_needed(hf_info.threshold, self.config.window)
|
&self.config.info,
|
||||||
{
|
) {
|
||||||
self.set_hf(new_hf);
|
self.current_hardfork = next_fork;
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets a new hard-fork.
|
|
||||||
fn set_hf(&mut self, new_hf: HardFork) {
|
|
||||||
self.next_hardfork = new_hf.next_fork();
|
|
||||||
self.current_hardfork = new_hf;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn current_hardfork(&self) -> HardFork {
|
pub fn current_hardfork(&self) -> HardFork {
|
||||||
self.current_hardfork
|
self.current_hardfork
|
||||||
}
|
}
|
||||||
|
@ -377,7 +146,7 @@ async fn get_votes_in_range<D: Database>(
|
||||||
database: D,
|
database: D,
|
||||||
block_heights: Range<u64>,
|
block_heights: Range<u64>,
|
||||||
window_size: usize,
|
window_size: usize,
|
||||||
) -> Result<HFVotes, ConsensusError> {
|
) -> Result<HFVotes, ExtendedConsensusError> {
|
||||||
let mut votes = HFVotes::new(window_size);
|
let mut votes = HFVotes::new(window_size);
|
||||||
|
|
||||||
let DatabaseResponse::BlockExtendedHeaderInRange(vote_list) = database
|
let DatabaseResponse::BlockExtendedHeaderInRange(vote_list) = database
|
||||||
|
|
|
@ -18,7 +18,7 @@ use tracing::instrument;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
helper::{median, rayon_spawn_async},
|
helper::{median, rayon_spawn_async},
|
||||||
ConsensusError, Database, DatabaseRequest, DatabaseResponse, HardFork,
|
Database, DatabaseRequest, DatabaseResponse, ExtendedConsensusError, HardFork,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -37,7 +37,7 @@ const LONG_TERM_WINDOW: u64 = 100000;
|
||||||
pub fn penalty_free_zone(hf: &HardFork) -> usize {
|
pub fn penalty_free_zone(hf: &HardFork) -> usize {
|
||||||
if hf == &HardFork::V1 {
|
if hf == &HardFork::V1 {
|
||||||
PENALTY_FREE_ZONE_1
|
PENALTY_FREE_ZONE_1
|
||||||
} else if hf.in_range(&HardFork::V2, &HardFork::V5) {
|
} else if hf >= &HardFork::V2 && hf < &HardFork::V5 {
|
||||||
PENALTY_FREE_ZONE_2
|
PENALTY_FREE_ZONE_2
|
||||||
} else {
|
} else {
|
||||||
PENALTY_FREE_ZONE_5
|
PENALTY_FREE_ZONE_5
|
||||||
|
@ -98,7 +98,7 @@ impl BlockWeightsCache {
|
||||||
chain_height: u64,
|
chain_height: u64,
|
||||||
config: BlockWeightsCacheConfig,
|
config: BlockWeightsCacheConfig,
|
||||||
database: D,
|
database: D,
|
||||||
) -> Result<Self, ConsensusError> {
|
) -> Result<Self, ExtendedConsensusError> {
|
||||||
tracing::info!("Initializing weight cache this may take a while.");
|
tracing::info!("Initializing weight cache this may take a while.");
|
||||||
|
|
||||||
let long_term_weights = get_long_term_weight_in_range(
|
let long_term_weights = get_long_term_weight_in_range(
|
||||||
|
@ -230,7 +230,7 @@ impl BlockWeightsCache {
|
||||||
///
|
///
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/blocks/reward.html#calculating-block-reward
|
/// https://cuprate.github.io/monero-book/consensus_rules/blocks/reward.html#calculating-block-reward
|
||||||
pub fn median_for_block_reward(&self, hf: &HardFork) -> usize {
|
pub fn median_for_block_reward(&self, hf: &HardFork) -> usize {
|
||||||
if hf.in_range(&HardFork::V1, &HardFork::V12) {
|
if hf < &HardFork::V12 {
|
||||||
self.median_short_term_weight()
|
self.median_short_term_weight()
|
||||||
} else {
|
} else {
|
||||||
self.effective_median_block_weight(hf)
|
self.effective_median_block_weight(hf)
|
||||||
|
@ -244,13 +244,13 @@ fn calculate_effective_median_block_weight(
|
||||||
median_short_term_weight: usize,
|
median_short_term_weight: usize,
|
||||||
median_long_term_weight: usize,
|
median_long_term_weight: usize,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
if hf.in_range(&HardFork::V1, &HardFork::V10) {
|
if hf < &HardFork::V10 {
|
||||||
return median_short_term_weight.max(penalty_free_zone(hf));
|
return median_short_term_weight.max(penalty_free_zone(hf));
|
||||||
}
|
}
|
||||||
|
|
||||||
let long_term_median = median_long_term_weight.max(PENALTY_FREE_ZONE_5);
|
let long_term_median = median_long_term_weight.max(PENALTY_FREE_ZONE_5);
|
||||||
let short_term_median = median_short_term_weight;
|
let short_term_median = median_short_term_weight;
|
||||||
let effective_median = if hf.in_range(&HardFork::V10, &HardFork::V15) {
|
let effective_median = if hf >= &HardFork::V10 && hf < &HardFork::V15 {
|
||||||
min(
|
min(
|
||||||
max(PENALTY_FREE_ZONE_5, short_term_median),
|
max(PENALTY_FREE_ZONE_5, short_term_median),
|
||||||
50 * long_term_median,
|
50 * long_term_median,
|
||||||
|
@ -270,14 +270,14 @@ pub fn calculate_block_long_term_weight(
|
||||||
block_weight: usize,
|
block_weight: usize,
|
||||||
long_term_median: usize,
|
long_term_median: usize,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
if hf.in_range(&HardFork::V1, &HardFork::V10) {
|
if hf < &HardFork::V10 {
|
||||||
return block_weight;
|
return block_weight;
|
||||||
}
|
}
|
||||||
|
|
||||||
let long_term_median = max(penalty_free_zone(hf), long_term_median);
|
let long_term_median = max(penalty_free_zone(hf), long_term_median);
|
||||||
|
|
||||||
let (short_term_constraint, adjusted_block_weight) =
|
let (short_term_constraint, adjusted_block_weight) =
|
||||||
if hf.in_range(&HardFork::V10, &HardFork::V15) {
|
if hf >= &HardFork::V10 && hf < &HardFork::V15 {
|
||||||
let stc = long_term_median + long_term_median * 2 / 5;
|
let stc = long_term_median + long_term_median * 2 / 5;
|
||||||
(stc, block_weight)
|
(stc, block_weight)
|
||||||
} else {
|
} else {
|
||||||
|
@ -292,7 +292,7 @@ pub fn calculate_block_long_term_weight(
|
||||||
async fn get_blocks_weight_in_range<D: Database + Clone>(
|
async fn get_blocks_weight_in_range<D: Database + Clone>(
|
||||||
range: Range<u64>,
|
range: Range<u64>,
|
||||||
database: D,
|
database: D,
|
||||||
) -> Result<Vec<usize>, ConsensusError> {
|
) -> Result<Vec<usize>, ExtendedConsensusError> {
|
||||||
tracing::info!("getting block weights.");
|
tracing::info!("getting block weights.");
|
||||||
|
|
||||||
let DatabaseResponse::BlockExtendedHeaderInRange(ext_headers) = database
|
let DatabaseResponse::BlockExtendedHeaderInRange(ext_headers) = database
|
||||||
|
@ -312,7 +312,7 @@ async fn get_blocks_weight_in_range<D: Database + Clone>(
|
||||||
async fn get_long_term_weight_in_range<D: Database + Clone>(
|
async fn get_long_term_weight_in_range<D: Database + Clone>(
|
||||||
range: Range<u64>,
|
range: Range<u64>,
|
||||||
database: D,
|
database: D,
|
||||||
) -> Result<Vec<usize>, ConsensusError> {
|
) -> Result<Vec<usize>, ExtendedConsensusError> {
|
||||||
tracing::info!("getting block long term weights.");
|
tracing::info!("getting block long term weights.");
|
||||||
|
|
||||||
let DatabaseResponse::BlockExtendedHeaderInRange(ext_headers) = database
|
let DatabaseResponse::BlockExtendedHeaderInRange(ext_headers) = database
|
||||||
|
|
|
@ -4,10 +4,11 @@ use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use monero_consensus::{transactions::OutputOnChain, ConsensusError, HardFork};
|
||||||
|
|
||||||
//mod batch_verifier;
|
//mod batch_verifier;
|
||||||
pub mod block;
|
pub mod block;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
pub mod genesis;
|
|
||||||
mod helper;
|
mod helper;
|
||||||
#[cfg(feature = "binaries")]
|
#[cfg(feature = "binaries")]
|
||||||
pub mod rpc;
|
pub mod rpc;
|
||||||
|
@ -20,10 +21,20 @@ pub use block::{
|
||||||
};
|
};
|
||||||
pub use context::{
|
pub use context::{
|
||||||
initialize_blockchain_context, BlockChainContext, BlockChainContextRequest,
|
initialize_blockchain_context, BlockChainContext, BlockChainContextRequest,
|
||||||
BlockChainContextResponse, ContextConfig, HardFork,
|
BlockChainContextResponse, ContextConfig,
|
||||||
};
|
};
|
||||||
pub use transactions::{VerifyTxRequest, VerifyTxResponse};
|
pub use transactions::{VerifyTxRequest, VerifyTxResponse};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ExtendedConsensusError {
|
||||||
|
#[error("{0}")]
|
||||||
|
ConErr(#[from] monero_consensus::ConsensusError),
|
||||||
|
#[error("Database error: {0}")]
|
||||||
|
DBErr(#[from] tower::BoxError),
|
||||||
|
#[error("Needed transaction is not in pool")]
|
||||||
|
TxPErr(#[from] TxNotInPool),
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: instead of (ab)using generic returns return the acc type
|
// TODO: instead of (ab)using generic returns return the acc type
|
||||||
pub async fn initialize_verifier<D, TxP, Ctx>(
|
pub async fn initialize_verifier<D, TxP, Ctx>(
|
||||||
database: D,
|
database: D,
|
||||||
|
@ -34,8 +45,8 @@ pub async fn initialize_verifier<D, TxP, Ctx>(
|
||||||
impl tower::Service<
|
impl tower::Service<
|
||||||
VerifyBlockRequest,
|
VerifyBlockRequest,
|
||||||
Response = VerifyBlockResponse,
|
Response = VerifyBlockResponse,
|
||||||
Error = ConsensusError,
|
Error = ExtendedConsensusError,
|
||||||
Future = impl Future<Output = Result<VerifyBlockResponse, ConsensusError>>
|
Future = impl Future<Output = Result<VerifyBlockResponse, ExtendedConsensusError>>
|
||||||
+ Send
|
+ Send
|
||||||
+ 'static,
|
+ 'static,
|
||||||
> + Clone
|
> + Clone
|
||||||
|
@ -44,8 +55,10 @@ pub async fn initialize_verifier<D, TxP, Ctx>(
|
||||||
impl tower::Service<
|
impl tower::Service<
|
||||||
VerifyTxRequest,
|
VerifyTxRequest,
|
||||||
Response = VerifyTxResponse,
|
Response = VerifyTxResponse,
|
||||||
Error = ConsensusError,
|
Error = ExtendedConsensusError,
|
||||||
Future = impl Future<Output = Result<VerifyTxResponse, ConsensusError>> + Send + 'static,
|
Future = impl Future<Output = Result<VerifyTxResponse, ExtendedConsensusError>>
|
||||||
|
+ Send
|
||||||
|
+ 'static,
|
||||||
> + Clone
|
> + Clone
|
||||||
+ Send
|
+ Send
|
||||||
+ 'static,
|
+ 'static,
|
||||||
|
@ -76,43 +89,6 @@ where
|
||||||
Ok((block_svc, tx_svc))
|
Ok((block_svc, tx_svc))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: split this enum up.
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum ConsensusError {
|
|
||||||
#[error("Miner transaction invalid: {0}")]
|
|
||||||
MinerTransaction(&'static str),
|
|
||||||
#[error("Transaction sig invalid: {0}")]
|
|
||||||
TransactionSignatureInvalid(&'static str),
|
|
||||||
#[error("Transaction has too high output amount")]
|
|
||||||
TransactionOutputsTooMuch,
|
|
||||||
#[error("Transaction inputs overflow")]
|
|
||||||
TransactionInputsOverflow,
|
|
||||||
#[error("Transaction outputs overflow")]
|
|
||||||
TransactionOutputsOverflow,
|
|
||||||
#[error("Transaction has an invalid output: {0}")]
|
|
||||||
TransactionInvalidOutput(&'static str),
|
|
||||||
#[error("Transaction has an invalid version")]
|
|
||||||
TransactionVersionInvalid,
|
|
||||||
#[error("Transaction an invalid input: {0}")]
|
|
||||||
TransactionHasInvalidInput(&'static str),
|
|
||||||
#[error("Transaction has invalid ring: {0}")]
|
|
||||||
TransactionHasInvalidRing(&'static str),
|
|
||||||
#[error("Block has an invalid proof of work")]
|
|
||||||
BlockPOWInvalid,
|
|
||||||
#[error("Block has a timestamp outside of the valid range")]
|
|
||||||
BlockTimestampInvalid,
|
|
||||||
#[error("Block is too large")]
|
|
||||||
BlockIsTooLarge,
|
|
||||||
#[error("Invalid hard fork version: {0}")]
|
|
||||||
InvalidHardForkVersion(&'static str),
|
|
||||||
#[error("The block has a different previous hash than expected")]
|
|
||||||
BlockIsNotApartOfChain,
|
|
||||||
#[error("One or more transaction is not in the transaction pool")]
|
|
||||||
TxNotInPool(#[from] TxNotInPool),
|
|
||||||
#[error("Database error: {0}")]
|
|
||||||
Database(#[from] tower::BoxError),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Database:
|
pub trait Database:
|
||||||
tower::Service<DatabaseRequest, Response = DatabaseResponse, Error = tower::BoxError>
|
tower::Service<DatabaseRequest, Response = DatabaseResponse, Error = tower::BoxError>
|
||||||
{
|
{
|
||||||
|
@ -123,14 +99,6 @@ impl<T: tower::Service<DatabaseRequest, Response = DatabaseResponse, Error = tow
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct OutputOnChain {
|
|
||||||
height: u64,
|
|
||||||
time_lock: monero_serai::transaction::Timelock,
|
|
||||||
key: curve25519_dalek::EdwardsPoint,
|
|
||||||
mask: curve25519_dalek::EdwardsPoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct ExtendedBlockHeader {
|
pub struct ExtendedBlockHeader {
|
||||||
pub version: HardFork,
|
pub version: HardFork,
|
||||||
|
@ -154,7 +122,7 @@ pub enum DatabaseRequest {
|
||||||
GeneratedCoins,
|
GeneratedCoins,
|
||||||
|
|
||||||
Outputs(HashMap<u64, HashSet<u64>>),
|
Outputs(HashMap<u64, HashSet<u64>>),
|
||||||
NumberOutputsWithAmount(u64),
|
NumberOutputsWithAmount(Vec<u64>),
|
||||||
|
|
||||||
CheckKIsNotSpent(HashSet<[u8; 32]>),
|
CheckKIsNotSpent(HashSet<[u8; 32]>),
|
||||||
|
|
||||||
|
@ -173,7 +141,7 @@ pub enum DatabaseResponse {
|
||||||
GeneratedCoins(u64),
|
GeneratedCoins(u64),
|
||||||
|
|
||||||
Outputs(HashMap<u64, HashMap<u64, OutputOnChain>>),
|
Outputs(HashMap<u64, HashMap<u64, OutputOnChain>>),
|
||||||
NumberOutputsWithAmount(usize),
|
NumberOutputsWithAmount(HashMap<u64, usize>),
|
||||||
|
|
||||||
/// returns true if key images are spent
|
/// returns true if key images are spent
|
||||||
CheckKIsNotSpent(bool),
|
CheckKIsNotSpent(bool),
|
||||||
|
|
|
@ -149,7 +149,7 @@ where
|
||||||
.boxed(),
|
.boxed(),
|
||||||
DatabaseRequest::NumberOutputsWithAmount(amt) => async move {
|
DatabaseRequest::NumberOutputsWithAmount(amt) => async move {
|
||||||
Ok(DatabaseResponse::NumberOutputsWithAmount(
|
Ok(DatabaseResponse::NumberOutputsWithAmount(
|
||||||
cache.read().await.numb_outs(amt),
|
cache.read().await.numb_outs(&amt),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
.boxed(),
|
.boxed(),
|
||||||
|
|
|
@ -115,8 +115,11 @@ impl ScanningCache {
|
||||||
self.numb_outs.values().sum()
|
self.numb_outs.values().sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn numb_outs(&self, amount: u64) -> usize {
|
pub fn numb_outs(&self, amounts: &[u64]) -> HashMap<u64, usize> {
|
||||||
*self.numb_outs.get(&amount).unwrap_or(&0)
|
amounts
|
||||||
|
.iter()
|
||||||
|
.map(|amount| (*amount, *self.numb_outs.get(amount).unwrap_or(&0)))
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_outs(&mut self, amount: u64, count: usize) {
|
pub fn add_outs(&mut self, amount: u64, count: usize) {
|
||||||
|
@ -130,7 +133,7 @@ impl ScanningCache {
|
||||||
|
|
||||||
impl Display for ScanningCache {
|
impl Display for ScanningCache {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
let rct_outs = self.numb_outs(0);
|
let rct_outs = *self.numb_outs(&[0]).get(&0).unwrap();
|
||||||
let total_outs = self.total_outs();
|
let total_outs = self.total_outs();
|
||||||
|
|
||||||
f.debug_struct("Cache")
|
f.debug_struct("Cache")
|
||||||
|
|
|
@ -131,9 +131,9 @@ impl RpcConnection {
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(ExtendedBlockHeader {
|
Ok(ExtendedBlockHeader {
|
||||||
version: HardFork::from_version(&info.major_version)
|
version: HardFork::from_version(info.major_version)
|
||||||
.expect("previously checked block has incorrect version"),
|
.expect("previously checked block has incorrect version"),
|
||||||
vote: HardFork::from_vote(&info.minor_version),
|
vote: HardFork::from_vote(info.minor_version),
|
||||||
timestamp: info.timestamp,
|
timestamp: info.timestamp,
|
||||||
cumulative_difficulty: u128_from_low_high(
|
cumulative_difficulty: u128_from_low_high(
|
||||||
info.cumulative_difficulty,
|
info.cumulative_difficulty,
|
||||||
|
@ -167,9 +167,9 @@ impl RpcConnection {
|
||||||
res.headers
|
res.headers
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|info| ExtendedBlockHeader {
|
.map(|info| ExtendedBlockHeader {
|
||||||
version: HardFork::from_version(&info.major_version)
|
version: HardFork::from_version(info.major_version)
|
||||||
.expect("previously checked block has incorrect version"),
|
.expect("previously checked block has incorrect version"),
|
||||||
vote: HardFork::from_vote(&info.minor_version),
|
vote: HardFork::from_vote(info.minor_version),
|
||||||
timestamp: info.timestamp,
|
timestamp: info.timestamp,
|
||||||
cumulative_difficulty: u128_from_low_high(
|
cumulative_difficulty: u128_from_low_high(
|
||||||
info.cumulative_difficulty,
|
info.cumulative_difficulty,
|
||||||
|
|
|
@ -19,7 +19,7 @@ use super::{
|
||||||
async fn check_rpc(addr: String, cache: Arc<RwLock<ScanningCache>>) -> Option<RpcConnectionSvc> {
|
async fn check_rpc(addr: String, cache: Arc<RwLock<ScanningCache>>) -> Option<RpcConnectionSvc> {
|
||||||
tracing::debug!("Sending request to node.");
|
tracing::debug!("Sending request to node.");
|
||||||
|
|
||||||
let con = HttpRpc::new_custom_timeout(addr.clone(), Duration::from_secs(u64::MAX))
|
let con = HttpRpc::with_custom_timeout(addr.clone(), Duration::from_secs(u64::MAX))
|
||||||
.await
|
.await
|
||||||
.ok()?;
|
.ok()?;
|
||||||
let (tx, rx) = mpsc::channel(0);
|
let (tx, rx) = mpsc::channel(0);
|
||||||
|
|
|
@ -13,32 +13,21 @@ use rayon::prelude::*;
|
||||||
use tower::{Service, ServiceExt};
|
use tower::{Service, ServiceExt};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
|
use monero_consensus::{
|
||||||
|
signatures::verify_contextual_signatures,
|
||||||
|
transactions::{
|
||||||
|
check_all_time_locks, check_inputs, check_outputs, check_tx_version, TransactionError,
|
||||||
|
TxRingMembersInfo,
|
||||||
|
},
|
||||||
|
ConsensusError, HardFork, TxVersion,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
context::ReOrgToken, helper::rayon_spawn_async, ConsensusError, Database, DatabaseRequest,
|
context::ReOrgToken, helper::rayon_spawn_async, Database, DatabaseRequest, DatabaseResponse,
|
||||||
DatabaseResponse, HardFork,
|
ExtendedConsensusError,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod contextual_data;
|
mod contextual_data;
|
||||||
mod inputs;
|
|
||||||
pub(crate) mod outputs;
|
|
||||||
mod sigs;
|
|
||||||
mod time_lock;
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
|
||||||
pub enum TxVersion {
|
|
||||||
RingSignatures,
|
|
||||||
RingCT,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TxVersion {
|
|
||||||
pub fn from_raw(version: u64) -> Result<TxVersion, ConsensusError> {
|
|
||||||
match version {
|
|
||||||
1 => Ok(TxVersion::RingSignatures),
|
|
||||||
2 => Ok(TxVersion::RingCT),
|
|
||||||
_ => Err(ConsensusError::TransactionVersionInvalid),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Data needed to verify a transaction.
|
/// Data needed to verify a transaction.
|
||||||
///
|
///
|
||||||
|
@ -52,7 +41,7 @@ pub struct TransactionVerificationData {
|
||||||
pub tx_hash: [u8; 32],
|
pub tx_hash: [u8; 32],
|
||||||
/// We put this behind a mutex as the information is not constant and is based of past outputs idxs
|
/// We put this behind a mutex as the information is not constant and is based of past outputs idxs
|
||||||
/// which could change on re-orgs.
|
/// which could change on re-orgs.
|
||||||
rings_member_info: std::sync::Mutex<Option<contextual_data::TxRingMembersInfo>>,
|
rings_member_info: std::sync::Mutex<Option<(TxRingMembersInfo, ReOrgToken)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TransactionVerificationData {
|
impl TransactionVerificationData {
|
||||||
|
@ -63,7 +52,8 @@ impl TransactionVerificationData {
|
||||||
tx_weight: tx.weight(),
|
tx_weight: tx.weight(),
|
||||||
fee: tx.rct_signatures.base.fee,
|
fee: tx.rct_signatures.base.fee,
|
||||||
rings_member_info: std::sync::Mutex::new(None),
|
rings_member_info: std::sync::Mutex::new(None),
|
||||||
version: TxVersion::from_raw(tx.prefix.version)?,
|
version: TxVersion::from_raw(tx.prefix.version)
|
||||||
|
.ok_or(TransactionError::TransactionVersionInvalid)?,
|
||||||
tx,
|
tx,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -113,7 +103,7 @@ where
|
||||||
D::Future: Send + 'static,
|
D::Future: Send + 'static,
|
||||||
{
|
{
|
||||||
type Response = VerifyTxResponse;
|
type Response = VerifyTxResponse;
|
||||||
type Error = ConsensusError;
|
type Error = ExtendedConsensusError;
|
||||||
type Future =
|
type Future =
|
||||||
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
|
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
|
||||||
|
|
||||||
|
@ -154,7 +144,7 @@ async fn batch_setup_transactions<D>(
|
||||||
txs: Vec<Transaction>,
|
txs: Vec<Transaction>,
|
||||||
hf: HardFork,
|
hf: HardFork,
|
||||||
re_org_token: ReOrgToken,
|
re_org_token: ReOrgToken,
|
||||||
) -> Result<VerifyTxResponse, ConsensusError>
|
) -> Result<VerifyTxResponse, ExtendedConsensusError>
|
||||||
where
|
where
|
||||||
D: Database + Clone + Sync + Send + 'static,
|
D: Database + Clone + Sync + Send + 'static,
|
||||||
{
|
{
|
||||||
|
@ -179,7 +169,7 @@ async fn verify_transactions_for_block<D>(
|
||||||
time_for_time_lock: u64,
|
time_for_time_lock: u64,
|
||||||
hf: HardFork,
|
hf: HardFork,
|
||||||
re_org_token: ReOrgToken,
|
re_org_token: ReOrgToken,
|
||||||
) -> Result<VerifyTxResponse, ConsensusError>
|
) -> Result<VerifyTxResponse, ExtendedConsensusError>
|
||||||
where
|
where
|
||||||
D: Database + Clone + Sync + Send + 'static,
|
D: Database + Clone + Sync + Send + 'static,
|
||||||
{
|
{
|
||||||
|
@ -215,9 +205,7 @@ where
|
||||||
};
|
};
|
||||||
|
|
||||||
if kis_spent {
|
if kis_spent {
|
||||||
return Err(ConsensusError::TransactionHasInvalidInput(
|
Err(ConsensusError::Transaction(TransactionError::KeyImageSpent))?;
|
||||||
"One or more key image spent!",
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(VerifyTxResponse::Ok)
|
Ok(VerifyTxResponse::Ok)
|
||||||
|
@ -243,21 +231,20 @@ fn verify_transaction_for_block(
|
||||||
None => panic!("rings_member_info needs to be set to be able to verify!"),
|
None => panic!("rings_member_info needs to be set to be able to verify!"),
|
||||||
};
|
};
|
||||||
|
|
||||||
check_tx_version(&rings_member_info.decoy_info, tx_version, &hf)?;
|
check_tx_version(&rings_member_info.0.decoy_info, tx_version, &hf)?;
|
||||||
|
|
||||||
time_lock::check_all_time_locks(
|
check_all_time_locks(
|
||||||
&rings_member_info.time_locked_outs,
|
&rings_member_info.0.time_locked_outs,
|
||||||
current_chain_height,
|
current_chain_height,
|
||||||
time_for_time_lock,
|
time_for_time_lock,
|
||||||
&hf,
|
&hf,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let sum_outputs =
|
let sum_outputs = check_outputs(&tx_verification_data.tx.prefix.outputs, &hf, tx_version)?;
|
||||||
outputs::check_outputs(&tx_verification_data.tx.prefix.outputs, &hf, tx_version)?;
|
|
||||||
|
|
||||||
let sum_inputs = inputs::check_inputs(
|
let sum_inputs = check_inputs(
|
||||||
&tx_verification_data.tx.prefix.inputs,
|
tx_verification_data.tx.prefix.inputs.as_slice(),
|
||||||
rings_member_info,
|
&rings_member_info.0,
|
||||||
current_chain_height,
|
current_chain_height,
|
||||||
&hf,
|
&hf,
|
||||||
tx_version,
|
tx_version,
|
||||||
|
@ -266,59 +253,14 @@ fn verify_transaction_for_block(
|
||||||
|
|
||||||
if tx_version == &TxVersion::RingSignatures {
|
if tx_version == &TxVersion::RingSignatures {
|
||||||
if sum_outputs >= sum_inputs {
|
if sum_outputs >= sum_inputs {
|
||||||
return Err(ConsensusError::TransactionOutputsTooMuch);
|
Err(TransactionError::OutputsTooHigh)?;
|
||||||
}
|
}
|
||||||
// check that monero-serai is calculating the correct value here, why can't we just use this
|
// check that monero-serai is calculating the correct value here, why can't we just use this
|
||||||
// value? because we don't have this when we create the object.
|
// value? because we don't have this when we create the object.
|
||||||
assert_eq!(tx_verification_data.fee, sum_inputs - sum_outputs);
|
assert_eq!(tx_verification_data.fee, sum_inputs - sum_outputs);
|
||||||
}
|
}
|
||||||
|
|
||||||
sigs::verify_signatures(&tx_verification_data.tx, &rings_member_info.rings)?;
|
verify_contextual_signatures(&tx_verification_data.tx, &rings_member_info.0.rings)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks the version is in the allowed range.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#version
|
|
||||||
fn check_tx_version(
|
|
||||||
decoy_info: &Option<contextual_data::DecoyInfo>,
|
|
||||||
version: &TxVersion,
|
|
||||||
hf: &HardFork,
|
|
||||||
) -> Result<(), ConsensusError> {
|
|
||||||
if let Some(decoy_info) = decoy_info {
|
|
||||||
let max = max_tx_version(hf);
|
|
||||||
if version > &max {
|
|
||||||
return Err(ConsensusError::TransactionVersionInvalid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Doc is wrong here
|
|
||||||
let min = min_tx_version(hf);
|
|
||||||
if version < &min && decoy_info.not_mixable != 0 {
|
|
||||||
return Err(ConsensusError::TransactionVersionInvalid);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// This will only happen for hard-fork 1 when only RingSignatures are allowed.
|
|
||||||
if version != &TxVersion::RingSignatures {
|
|
||||||
return Err(ConsensusError::TransactionVersionInvalid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn max_tx_version(hf: &HardFork) -> TxVersion {
|
|
||||||
if hf <= &HardFork::V3 {
|
|
||||||
TxVersion::RingSignatures
|
|
||||||
} else {
|
|
||||||
TxVersion::RingCT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn min_tx_version(hf: &HardFork) -> TxVersion {
|
|
||||||
if hf >= &HardFork::V6 {
|
|
||||||
TxVersion::RingCT
|
|
||||||
} else {
|
|
||||||
TxVersion::RingSignatures
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -11,30 +11,30 @@
|
||||||
//! Because this data is unique for *every* transaction and the context service is just for blockchain state data.
|
//! Because this data is unique for *every* transaction and the context service is just for blockchain state data.
|
||||||
//!
|
//!
|
||||||
|
|
||||||
use std::ops::Deref;
|
use std::collections::HashSet;
|
||||||
use std::{
|
use std::{collections::HashMap, ops::Deref, sync::Arc};
|
||||||
cmp::{max, min},
|
|
||||||
collections::{HashMap, HashSet},
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
use curve25519_dalek::EdwardsPoint;
|
use monero_serai::transaction::Input;
|
||||||
use monero_serai::transaction::{Input, Timelock};
|
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
|
|
||||||
use crate::{
|
use monero_consensus::{
|
||||||
context::ReOrgToken, transactions::TransactionVerificationData, ConsensusError, Database,
|
transactions::{
|
||||||
DatabaseRequest, DatabaseResponse, HardFork, OutputOnChain,
|
get_ring_members_for_inputs, insert_ring_member_ids, DecoyInfo, TxRingMembersInfo,
|
||||||
|
},
|
||||||
|
ConsensusError, HardFork,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::TxVersion;
|
use crate::{
|
||||||
|
context::ReOrgToken, transactions::TransactionVerificationData, Database, DatabaseRequest,
|
||||||
|
DatabaseResponse, ExtendedConsensusError,
|
||||||
|
};
|
||||||
|
|
||||||
pub async fn batch_refresh_ring_member_info<D: Database + Clone + Send + Sync + 'static>(
|
pub async fn batch_refresh_ring_member_info<D: Database + Clone + Send + Sync + 'static>(
|
||||||
txs_verification_data: &[Arc<TransactionVerificationData>],
|
txs_verification_data: &[Arc<TransactionVerificationData>],
|
||||||
hf: &HardFork,
|
hf: &HardFork,
|
||||||
re_org_token: ReOrgToken,
|
re_org_token: ReOrgToken,
|
||||||
database: D,
|
mut database: D,
|
||||||
) -> Result<(), ConsensusError> {
|
) -> Result<(), ExtendedConsensusError> {
|
||||||
let (txs_needing_full_refresh, txs_needing_partial_refresh) =
|
let (txs_needing_full_refresh, txs_needing_partial_refresh) =
|
||||||
ring_member_info_needing_refresh(txs_verification_data, hf);
|
ring_member_info_needing_refresh(txs_verification_data, hf);
|
||||||
|
|
||||||
|
@ -48,10 +48,40 @@ pub async fn batch_refresh_ring_member_info<D: Database + Clone + Send + Sync +
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let unique_input_amounts = txs_needing_partial_refresh
|
||||||
|
.iter()
|
||||||
|
.flat_map(|tx_info| {
|
||||||
|
tx_info
|
||||||
|
.tx
|
||||||
|
.prefix
|
||||||
|
.inputs
|
||||||
|
.iter()
|
||||||
|
.map(|input| match input {
|
||||||
|
Input::ToKey { amount, .. } => amount.unwrap_or(0),
|
||||||
|
_ => 0,
|
||||||
|
})
|
||||||
|
.collect::<HashSet<_>>()
|
||||||
|
})
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
let DatabaseResponse::NumberOutputsWithAmount(outputs_with_amount) = database
|
||||||
|
.ready()
|
||||||
|
.await?
|
||||||
|
.call(DatabaseRequest::NumberOutputsWithAmount(
|
||||||
|
unique_input_amounts.into_iter().collect(),
|
||||||
|
))
|
||||||
|
.await?
|
||||||
|
else {
|
||||||
|
panic!("Database sent incorrect response!")
|
||||||
|
};
|
||||||
|
|
||||||
for tx_v_data in txs_needing_partial_refresh {
|
for tx_v_data in txs_needing_partial_refresh {
|
||||||
let decoy_info = if hf != &HardFork::V1 {
|
let decoy_info = if hf != &HardFork::V1 {
|
||||||
// this data is only needed after hard-fork 1.
|
// this data is only needed after hard-fork 1.
|
||||||
Some(DecoyInfo::new(&tx_v_data.tx.prefix.inputs, hf, database.clone()).await?)
|
Some(
|
||||||
|
DecoyInfo::new(&tx_v_data.tx.prefix.inputs, &outputs_with_amount, hf)
|
||||||
|
.map_err(ConsensusError::Transaction)?,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
@ -64,6 +94,7 @@ pub async fn batch_refresh_ring_member_info<D: Database + Clone + Send + Sync +
|
||||||
.as_mut()
|
.as_mut()
|
||||||
// this unwrap is safe as otherwise this would require a full refresh not a partial one.
|
// this unwrap is safe as otherwise this would require a full refresh not a partial one.
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
.0
|
||||||
.decoy_info = decoy_info;
|
.decoy_info = decoy_info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,7 +123,7 @@ fn ring_member_info_needing_refresh(
|
||||||
|
|
||||||
// if we don't have ring members or if a re-org has happened do a full refresh.
|
// if we don't have ring members or if a re-org has happened do a full refresh.
|
||||||
if let Some(tx_ring_member_info) = tx_ring_member_info.deref() {
|
if let Some(tx_ring_member_info) = tx_ring_member_info.deref() {
|
||||||
if tx_ring_member_info.re_org_token.reorg_happened() {
|
if tx_ring_member_info.1.reorg_happened() {
|
||||||
txs_needing_full_refresh.push(tx.clone());
|
txs_needing_full_refresh.push(tx.clone());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -107,6 +138,7 @@ fn ring_member_info_needing_refresh(
|
||||||
if &tx_ring_member_info
|
if &tx_ring_member_info
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("We just checked if this was None")
|
.expect("We just checked if this was None")
|
||||||
|
.0
|
||||||
.hf
|
.hf
|
||||||
!= hf
|
!= hf
|
||||||
|| tx.tx.prefix.inputs.iter().any(|inp| match inp {
|
|| tx.tx.prefix.inputs.iter().any(|inp| match inp {
|
||||||
|
@ -130,11 +162,12 @@ pub async fn batch_fill_ring_member_info<D: Database + Clone + Send + Sync + 'st
|
||||||
hf: &HardFork,
|
hf: &HardFork,
|
||||||
re_org_token: ReOrgToken,
|
re_org_token: ReOrgToken,
|
||||||
mut database: D,
|
mut database: D,
|
||||||
) -> Result<(), ConsensusError> {
|
) -> Result<(), ExtendedConsensusError> {
|
||||||
let mut output_ids = HashMap::new();
|
let mut output_ids = HashMap::new();
|
||||||
|
|
||||||
for tx_v_data in txs_verification_data.iter() {
|
for tx_v_data in txs_verification_data.iter() {
|
||||||
insert_ring_member_ids(&tx_v_data.tx.prefix.inputs, &mut output_ids)?;
|
insert_ring_member_ids(&tx_v_data.tx.prefix.inputs, &mut output_ids)
|
||||||
|
.map_err(ConsensusError::Transaction)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let DatabaseResponse::Outputs(outputs) = database
|
let DatabaseResponse::Outputs(outputs) = database
|
||||||
|
@ -146,322 +179,38 @@ pub async fn batch_fill_ring_member_info<D: Database + Clone + Send + Sync + 'st
|
||||||
panic!("Database sent incorrect response!")
|
panic!("Database sent incorrect response!")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let DatabaseResponse::NumberOutputsWithAmount(outputs_with_amount) = database
|
||||||
|
.ready()
|
||||||
|
.await?
|
||||||
|
.call(DatabaseRequest::NumberOutputsWithAmount(
|
||||||
|
outputs.keys().copied().collect(),
|
||||||
|
))
|
||||||
|
.await?
|
||||||
|
else {
|
||||||
|
panic!("Database sent incorrect response!")
|
||||||
|
};
|
||||||
|
|
||||||
for tx_v_data in txs_verification_data {
|
for tx_v_data in txs_verification_data {
|
||||||
let ring_members_for_tx =
|
let ring_members_for_tx =
|
||||||
get_ring_members_for_inputs(&outputs, &tx_v_data.tx.prefix.inputs)?;
|
get_ring_members_for_inputs(&outputs, &tx_v_data.tx.prefix.inputs)
|
||||||
|
.map_err(ConsensusError::Transaction)?;
|
||||||
|
|
||||||
let decoy_info = if hf != &HardFork::V1 {
|
let decoy_info = if hf != &HardFork::V1 {
|
||||||
// this data is only needed after hard-fork 1.
|
// this data is only needed after hard-fork 1.
|
||||||
Some(DecoyInfo::new(&tx_v_data.tx.prefix.inputs, hf, database.clone()).await?)
|
Some(
|
||||||
|
DecoyInfo::new(&tx_v_data.tx.prefix.inputs, &outputs_with_amount, hf)
|
||||||
|
.map_err(ConsensusError::Transaction)?,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// Temporarily acquirer the mutex lock to add the ring member info.
|
// Temporarily acquirer the mutex lock to add the ring member info.
|
||||||
let _ = tx_v_data
|
let _ = tx_v_data.rings_member_info.lock().unwrap().insert((
|
||||||
.rings_member_info
|
TxRingMembersInfo::new(ring_members_for_tx, decoy_info, tx_v_data.version, *hf),
|
||||||
.lock()
|
re_org_token.clone(),
|
||||||
.unwrap()
|
));
|
||||||
.insert(TxRingMembersInfo::new(
|
|
||||||
ring_members_for_tx,
|
|
||||||
decoy_info,
|
|
||||||
tx_v_data.version,
|
|
||||||
*hf,
|
|
||||||
re_org_token.clone(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the absolute offsets from the relative offsets.
|
|
||||||
///
|
|
||||||
/// This function will return an error if the relative offsets are empty.
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#inputs-must-have-decoys
|
|
||||||
fn get_absolute_offsets(relative_offsets: &[u64]) -> Result<Vec<u64>, ConsensusError> {
|
|
||||||
if relative_offsets.is_empty() {
|
|
||||||
return Err(ConsensusError::TransactionHasInvalidRing(
|
|
||||||
"ring has no members",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut offsets = Vec::with_capacity(relative_offsets.len());
|
|
||||||
offsets.push(relative_offsets[0]);
|
|
||||||
|
|
||||||
for i in 1..relative_offsets.len() {
|
|
||||||
offsets.push(offsets[i - 1] + relative_offsets[i]);
|
|
||||||
}
|
|
||||||
Ok(offsets)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Inserts the output IDs that are needed to verify the transaction inputs into the provided HashMap.
|
|
||||||
///
|
|
||||||
/// This will error if the inputs are empty
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#no-empty-inputs
|
|
||||||
///
|
|
||||||
fn insert_ring_member_ids(
|
|
||||||
inputs: &[Input],
|
|
||||||
output_ids: &mut HashMap<u64, HashSet<u64>>,
|
|
||||||
) -> Result<(), ConsensusError> {
|
|
||||||
if inputs.is_empty() {
|
|
||||||
return Err(ConsensusError::TransactionHasInvalidInput(
|
|
||||||
"transaction has no inputs",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
for input in inputs {
|
|
||||||
match input {
|
|
||||||
Input::ToKey {
|
|
||||||
amount,
|
|
||||||
key_offsets,
|
|
||||||
..
|
|
||||||
} => output_ids
|
|
||||||
.entry(amount.unwrap_or(0))
|
|
||||||
.or_default()
|
|
||||||
.extend(get_absolute_offsets(key_offsets)?),
|
|
||||||
// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#input-type
|
|
||||||
_ => {
|
|
||||||
return Err(ConsensusError::TransactionHasInvalidInput(
|
|
||||||
"input not ToKey",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the ring members of all the inputs.
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum Rings {
|
|
||||||
/// Legacy, pre-ringCT, rings.
|
|
||||||
Legacy(Vec<Vec<EdwardsPoint>>),
|
|
||||||
// RingCT rings, (outkey, mask).
|
|
||||||
RingCT(Vec<Vec<[EdwardsPoint; 2]>>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Rings {
|
|
||||||
/// Builds the rings for the transaction inputs, from the given outputs.
|
|
||||||
fn new(outputs: Vec<Vec<&OutputOnChain>>, tx_version: TxVersion) -> Rings {
|
|
||||||
match tx_version {
|
|
||||||
TxVersion::RingSignatures => Rings::Legacy(
|
|
||||||
outputs
|
|
||||||
.into_iter()
|
|
||||||
.map(|inp_outs| inp_outs.into_iter().map(|out| out.key).collect())
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
TxVersion::RingCT => Rings::RingCT(
|
|
||||||
outputs
|
|
||||||
.into_iter()
|
|
||||||
.map(|inp_outs| {
|
|
||||||
inp_outs
|
|
||||||
.into_iter()
|
|
||||||
.map(|out| [out.key, out.mask])
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Information on the outputs the transaction is is referencing for inputs (ring members).
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct TxRingMembersInfo {
|
|
||||||
pub rings: Rings,
|
|
||||||
/// Information on the structure of the decoys, will be [`None`] for txs before [`HardFork::V1`]
|
|
||||||
pub decoy_info: Option<DecoyInfo>,
|
|
||||||
pub youngest_used_out_height: u64,
|
|
||||||
pub time_locked_outs: Vec<Timelock>,
|
|
||||||
/// A token used to check if a re org has happened since getting this data.
|
|
||||||
re_org_token: ReOrgToken,
|
|
||||||
/// The hard-fork this data was retrived for.
|
|
||||||
hf: HardFork,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TxRingMembersInfo {
|
|
||||||
/// Construct a [`TxRingMembersInfo`] struct.
|
|
||||||
///
|
|
||||||
/// The used outs must be all the ring members used in the transactions inputs.
|
|
||||||
fn new(
|
|
||||||
used_outs: Vec<Vec<&OutputOnChain>>,
|
|
||||||
decoy_info: Option<DecoyInfo>,
|
|
||||||
tx_version: TxVersion,
|
|
||||||
hf: HardFork,
|
|
||||||
re_org_token: ReOrgToken,
|
|
||||||
) -> TxRingMembersInfo {
|
|
||||||
TxRingMembersInfo {
|
|
||||||
youngest_used_out_height: used_outs
|
|
||||||
.iter()
|
|
||||||
.map(|inp_outs| {
|
|
||||||
inp_outs
|
|
||||||
.iter()
|
|
||||||
// the output with the highest height is the youngest
|
|
||||||
.map(|out| out.height)
|
|
||||||
.max()
|
|
||||||
.expect("Input must have ring members")
|
|
||||||
})
|
|
||||||
.max()
|
|
||||||
.expect("Tx must have inputs"),
|
|
||||||
time_locked_outs: used_outs
|
|
||||||
.iter()
|
|
||||||
.flat_map(|inp_outs| {
|
|
||||||
inp_outs
|
|
||||||
.iter()
|
|
||||||
.filter_map(|out| match out.time_lock {
|
|
||||||
Timelock::None => None,
|
|
||||||
lock => Some(lock),
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
rings: Rings::new(used_outs, tx_version),
|
|
||||||
re_org_token,
|
|
||||||
decoy_info,
|
|
||||||
hf,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the ring members for the inputs from the outputs on the chain.
|
|
||||||
///
|
|
||||||
/// Will error if `outputs` does not contain the outputs needed.
|
|
||||||
fn get_ring_members_for_inputs<'a>(
|
|
||||||
outputs: &'a HashMap<u64, HashMap<u64, OutputOnChain>>,
|
|
||||||
inputs: &[Input],
|
|
||||||
) -> Result<Vec<Vec<&'a OutputOnChain>>, ConsensusError> {
|
|
||||||
inputs
|
|
||||||
.iter()
|
|
||||||
.map(|inp| match inp {
|
|
||||||
Input::ToKey {
|
|
||||||
amount,
|
|
||||||
key_offsets,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let offsets = get_absolute_offsets(key_offsets)?;
|
|
||||||
Ok(offsets
|
|
||||||
.iter()
|
|
||||||
.map(|offset| {
|
|
||||||
// get the hashmap for this amount.
|
|
||||||
outputs
|
|
||||||
.get(&amount.unwrap_or(0))
|
|
||||||
// get output at the index from the amount hashmap.
|
|
||||||
.and_then(|amount_map| amount_map.get(offset))
|
|
||||||
.ok_or(ConsensusError::TransactionHasInvalidRing(
|
|
||||||
"ring member not in database",
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.collect::<Result<_, ConsensusError>>()?)
|
|
||||||
}
|
|
||||||
_ => Err(ConsensusError::TransactionHasInvalidInput(
|
|
||||||
"input not ToKey",
|
|
||||||
)),
|
|
||||||
})
|
|
||||||
.collect::<Result<_, ConsensusError>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A struct holding information about the inputs and their decoys. This data can vary by block so
|
|
||||||
/// this data needs to be retrieved after every change in the blockchain.
|
|
||||||
///
|
|
||||||
/// This data *does not* need to be refreshed if one of these are true:
|
|
||||||
/// - The input amounts are *ALL* 0
|
|
||||||
/// - The top block hash is the same as when this data was retrieved.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct DecoyInfo {
|
|
||||||
/// The number of inputs that have enough outputs on the chain to mix with.
|
|
||||||
pub mixable: usize,
|
|
||||||
/// The number of inputs that don't have enough outputs on the chain to mix with.
|
|
||||||
pub not_mixable: usize,
|
|
||||||
/// The minimum amount of decoys used in the transaction.
|
|
||||||
pub min_decoys: usize,
|
|
||||||
/// The maximum amount of decoys used in the transaction.
|
|
||||||
pub max_decoys: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DecoyInfo {
|
|
||||||
/// Creates a new [`DecoyInfo`] struct relating to the passed in inputs.
|
|
||||||
///
|
|
||||||
/// Do not rely on this function to do consensus checks!
|
|
||||||
///
|
|
||||||
pub async fn new<D: Database>(
|
|
||||||
inputs: &[Input],
|
|
||||||
hf: &HardFork,
|
|
||||||
mut database: D,
|
|
||||||
) -> Result<DecoyInfo, ConsensusError> {
|
|
||||||
let mut min_decoys = usize::MAX;
|
|
||||||
let mut max_decoys = usize::MIN;
|
|
||||||
let mut mixable = 0;
|
|
||||||
let mut not_mixable = 0;
|
|
||||||
|
|
||||||
let minimum_decoys = minimum_decoys(hf);
|
|
||||||
|
|
||||||
for inp in inputs {
|
|
||||||
match inp {
|
|
||||||
Input::ToKey {
|
|
||||||
amount,
|
|
||||||
key_offsets,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
if let Some(amt) = *amount {
|
|
||||||
let DatabaseResponse::NumberOutputsWithAmount(numb_of_outs) = database
|
|
||||||
.ready()
|
|
||||||
.await?
|
|
||||||
.call(DatabaseRequest::NumberOutputsWithAmount(amt))
|
|
||||||
.await?
|
|
||||||
else {
|
|
||||||
panic!("Database sent incorrect response!");
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html#mixable-and-unmixable-inputs
|
|
||||||
if numb_of_outs <= minimum_decoys && amt != 0 {
|
|
||||||
not_mixable += 1;
|
|
||||||
} else {
|
|
||||||
mixable += 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// ringCT amounts are always mixable.
|
|
||||||
mixable += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let numb_decoys = key_offsets
|
|
||||||
.len()
|
|
||||||
.checked_sub(1)
|
|
||||||
.ok_or(ConsensusError::TransactionHasInvalidRing("ring is empty"))?;
|
|
||||||
// https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html#minimum-and-maximum-decoys-used
|
|
||||||
min_decoys = min(min_decoys, numb_decoys);
|
|
||||||
max_decoys = max(max_decoys, numb_decoys);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Err(ConsensusError::TransactionHasInvalidInput(
|
|
||||||
"input not ToKey",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(DecoyInfo {
|
|
||||||
mixable,
|
|
||||||
not_mixable,
|
|
||||||
min_decoys,
|
|
||||||
max_decoys,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the minimum amount of decoys for a hard-fork.
|
|
||||||
/// **There are exceptions to this always being the minimum decoys**
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html#minimum-amount-of-decoys
|
|
||||||
pub(crate) fn minimum_decoys(hf: &HardFork) -> usize {
|
|
||||||
use HardFork as HF;
|
|
||||||
match hf {
|
|
||||||
HF::V1 => panic!("hard-fork 1 does not use these rules!"),
|
|
||||||
HF::V2 | HF::V3 | HF::V4 | HF::V5 => 2,
|
|
||||||
HF::V6 => 4,
|
|
||||||
HF::V7 => 6,
|
|
||||||
HF::V8 | HF::V9 | HF::V10 | HF::V11 | HF::V12 | HF::V13 | HF::V14 => 10,
|
|
||||||
HF::V15 | HF::V16 => 15,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,254 +0,0 @@
|
||||||
//! # Inputs
|
|
||||||
//!
|
|
||||||
//! This module contains all consensus rules for non-miner transaction inputs, excluding time locks.
|
|
||||||
//!
|
|
||||||
|
|
||||||
use std::{cmp::Ordering, collections::HashSet, sync::Arc};
|
|
||||||
|
|
||||||
use monero_serai::transaction::Input;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
transactions::{
|
|
||||||
contextual_data::{minimum_decoys, DecoyInfo, TxRingMembersInfo},
|
|
||||||
TxVersion,
|
|
||||||
},
|
|
||||||
ConsensusError, HardFork,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Checks the decoys are allowed.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#minimum-decoys
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#equal-number-of-decoys
|
|
||||||
fn check_decoy_info(decoy_info: &DecoyInfo, hf: &HardFork) -> Result<(), ConsensusError> {
|
|
||||||
if hf == &HardFork::V15 {
|
|
||||||
// Hard-fork 15 allows both v14 and v16 rules
|
|
||||||
return check_decoy_info(decoy_info, &HardFork::V14)
|
|
||||||
.or_else(|_| check_decoy_info(decoy_info, &HardFork::V16));
|
|
||||||
}
|
|
||||||
|
|
||||||
let current_minimum_decoys = minimum_decoys(hf);
|
|
||||||
|
|
||||||
if decoy_info.min_decoys < current_minimum_decoys {
|
|
||||||
if decoy_info.not_mixable == 0 {
|
|
||||||
return Err(ConsensusError::TransactionHasInvalidRing(
|
|
||||||
"input does not have enough decoys",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if decoy_info.mixable > 1 {
|
|
||||||
return Err(ConsensusError::TransactionHasInvalidInput(
|
|
||||||
"more than one mixable input with unmixable inputs",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hf >= &HardFork::V8 && decoy_info.min_decoys != current_minimum_decoys {
|
|
||||||
return Err(ConsensusError::TransactionHasInvalidRing(
|
|
||||||
"one ring does not have the minimum number of decoys",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if hf >= &HardFork::V12 && decoy_info.min_decoys != decoy_info.max_decoys {
|
|
||||||
return Err(ConsensusError::TransactionHasInvalidRing(
|
|
||||||
"rings do not have the same number of members",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks the inputs key images for torsion and for duplicates in the transaction.
|
|
||||||
///
|
|
||||||
/// The `spent_kis` parameter is not meant to be a complete list of key images, just a list of related transactions
|
|
||||||
/// key images, for example transactions in a block. The chain will be checked for duplicates later.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#unique-key-image
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#torsion-free-key-image
|
|
||||||
pub(crate) fn check_key_images(
|
|
||||||
input: &Input,
|
|
||||||
spent_kis: &mut HashSet<[u8; 32]>,
|
|
||||||
) -> Result<(), ConsensusError> {
|
|
||||||
match input {
|
|
||||||
Input::ToKey { key_image, .. } => {
|
|
||||||
// this happens in monero-serai but we may as well duplicate the check.
|
|
||||||
if !key_image.is_torsion_free() {
|
|
||||||
return Err(ConsensusError::TransactionHasInvalidInput(
|
|
||||||
"key image has torsion",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if !spent_kis.insert(key_image.compress().to_bytes()) {
|
|
||||||
return Err(ConsensusError::TransactionHasInvalidInput(
|
|
||||||
"key image already spent",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Err(ConsensusError::TransactionHasInvalidInput(
|
|
||||||
"Input not ToKey",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks that the input is of type [`Input::ToKey`] aka txin_to_key.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#input-type
|
|
||||||
fn check_input_type(input: &Input) -> Result<(), ConsensusError> {
|
|
||||||
match input {
|
|
||||||
Input::ToKey { .. } => Ok(()),
|
|
||||||
_ => Err(ConsensusError::TransactionHasInvalidInput(
|
|
||||||
"Input not ToKey",
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks that the input has decoys.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#inputs-must-have-decoys
|
|
||||||
fn check_input_has_decoys(input: &Input) -> Result<(), ConsensusError> {
|
|
||||||
match input {
|
|
||||||
Input::ToKey { key_offsets, .. } => {
|
|
||||||
if key_offsets.is_empty() {
|
|
||||||
Err(ConsensusError::TransactionHasInvalidRing("No ring members"))
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => panic!("Input not ToKey"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks that the ring members for the input are unique after hard-fork 6.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#unique-ring-members
|
|
||||||
fn check_ring_members_unique(input: &Input, hf: &HardFork) -> Result<(), ConsensusError> {
|
|
||||||
if hf >= &HardFork::V6 {
|
|
||||||
match input {
|
|
||||||
Input::ToKey { key_offsets, .. } => key_offsets.iter().skip(1).try_for_each(|offset| {
|
|
||||||
if *offset == 0 {
|
|
||||||
Err(ConsensusError::TransactionHasInvalidRing(
|
|
||||||
"duplicate ring member",
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
_ => panic!("Only ToKey is allowed this should have already been checked!"),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks that from hf 7 the inputs are sorted by key image.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#sorted-inputs
|
|
||||||
fn check_inputs_sorted(inputs: &[Input], hf: &HardFork) -> Result<(), ConsensusError> {
|
|
||||||
let get_ki = |inp: &Input| match inp {
|
|
||||||
Input::ToKey { key_image, .. } => key_image.compress().to_bytes(),
|
|
||||||
_ => panic!("Only ToKey is allowed this should have already been checked!"),
|
|
||||||
};
|
|
||||||
|
|
||||||
if hf >= &HardFork::V7 {
|
|
||||||
for inps in inputs.windows(2) {
|
|
||||||
match get_ki(&inps[0]).cmp(&get_ki(&inps[1])) {
|
|
||||||
Ordering::Less => (),
|
|
||||||
_ => {
|
|
||||||
return Err(ConsensusError::TransactionHasInvalidInput(
|
|
||||||
"Inputs not ordered by key image!",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks the youngest output is at least 10 blocks old.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#10-block-lock
|
|
||||||
fn check_10_block_lock(
|
|
||||||
ring_member_info: &TxRingMembersInfo,
|
|
||||||
current_chain_height: u64,
|
|
||||||
hf: &HardFork,
|
|
||||||
) -> Result<(), ConsensusError> {
|
|
||||||
if hf >= &HardFork::V12 {
|
|
||||||
if ring_member_info.youngest_used_out_height + 10 > current_chain_height {
|
|
||||||
Err(ConsensusError::TransactionHasInvalidRing(
|
|
||||||
"tx has one ring member which is too young",
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sums the inputs checking for overflow.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/pre_rct.html#inputs-and-outputs-must-not-overflow
|
|
||||||
fn sum_inputs_v1(inputs: &[Input]) -> Result<u64, ConsensusError> {
|
|
||||||
let mut sum: u64 = 0;
|
|
||||||
for inp in inputs {
|
|
||||||
match inp {
|
|
||||||
Input::ToKey { amount, .. } => {
|
|
||||||
sum = sum
|
|
||||||
.checked_add(amount.unwrap_or(0))
|
|
||||||
.ok_or(ConsensusError::TransactionInputsOverflow)?;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Err(ConsensusError::TransactionHasInvalidInput(
|
|
||||||
"input not ToKey",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(sum)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks all input consensus rules.
|
|
||||||
///
|
|
||||||
/// TODO: list rules.
|
|
||||||
///
|
|
||||||
pub fn check_inputs(
|
|
||||||
inputs: &[Input],
|
|
||||||
ring_member_info: &TxRingMembersInfo,
|
|
||||||
current_chain_height: u64,
|
|
||||||
hf: &HardFork,
|
|
||||||
tx_version: &TxVersion,
|
|
||||||
spent_kis: Arc<std::sync::Mutex<HashSet<[u8; 32]>>>,
|
|
||||||
) -> Result<u64, ConsensusError> {
|
|
||||||
if inputs.is_empty() {
|
|
||||||
return Err(ConsensusError::TransactionHasInvalidInput("no inputs"));
|
|
||||||
}
|
|
||||||
|
|
||||||
check_10_block_lock(ring_member_info, current_chain_height, hf)?;
|
|
||||||
|
|
||||||
if let Some(decoy_info) = &ring_member_info.decoy_info {
|
|
||||||
check_decoy_info(decoy_info, hf)?;
|
|
||||||
} else {
|
|
||||||
assert_eq!(hf, &HardFork::V1);
|
|
||||||
}
|
|
||||||
|
|
||||||
for input in inputs {
|
|
||||||
check_input_type(input)?;
|
|
||||||
check_input_has_decoys(input)?;
|
|
||||||
|
|
||||||
check_ring_members_unique(input, hf)?;
|
|
||||||
|
|
||||||
let mut spent_kis_lock = spent_kis.lock().unwrap();
|
|
||||||
check_key_images(input, &mut spent_kis_lock)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
check_inputs_sorted(inputs, hf)?;
|
|
||||||
|
|
||||||
match tx_version {
|
|
||||||
TxVersion::RingSignatures => sum_inputs_v1(inputs),
|
|
||||||
_ => panic!("TODO: RCT"),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,141 +0,0 @@
|
||||||
//! # Outputs
|
|
||||||
//!
|
|
||||||
//! Consensus rules relating to non-miner transaction outputs
|
|
||||||
|
|
||||||
use std::sync::OnceLock;
|
|
||||||
|
|
||||||
use monero_serai::transaction::Output;
|
|
||||||
|
|
||||||
use crate::{helper::check_point, transactions::TxVersion, ConsensusError, HardFork};
|
|
||||||
|
|
||||||
/// Decomposed amount table.
|
|
||||||
///
|
|
||||||
/// TODO: manually list each amount
|
|
||||||
static DECOMPOSED_AMOUNTS: OnceLock<[u64; 172]> = OnceLock::new();
|
|
||||||
|
|
||||||
pub(crate) fn decomposed_amounts() -> &'static [u64; 172] {
|
|
||||||
DECOMPOSED_AMOUNTS.get_or_init(|| {
|
|
||||||
let mut amounts = [1; 172];
|
|
||||||
for zeros in 0..19 {
|
|
||||||
for digit in 1..10 {
|
|
||||||
amounts[zeros * 9 + digit - 1] =
|
|
||||||
(digit * 10_usize.pow(zeros as u32)).try_into().unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
amounts[171] = 10000000000000000000;
|
|
||||||
amounts
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks the output keys are canonical points.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#output-keys-canonical
|
|
||||||
fn check_output_keys(outputs: &[Output]) -> Result<(), ConsensusError> {
|
|
||||||
for out in outputs {
|
|
||||||
if !check_point(&out.key) {
|
|
||||||
return Err(ConsensusError::TransactionInvalidOutput(
|
|
||||||
"outputs one time key is not a valid point",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks the output types are allowed.
|
|
||||||
///
|
|
||||||
/// This is also used during miner-tx verification.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#output-type
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#output-type
|
|
||||||
pub(crate) fn check_output_types(outputs: &[Output], hf: &HardFork) -> Result<(), ConsensusError> {
|
|
||||||
if hf == &HardFork::V15 {
|
|
||||||
for outs in outputs.windows(2) {
|
|
||||||
if outs[0].view_tag.is_some() != outs[0].view_tag.is_some() {
|
|
||||||
return Err(ConsensusError::TransactionInvalidOutput(
|
|
||||||
"v15 output must have one output type",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
for out in outputs {
|
|
||||||
if hf <= &HardFork::V14 && out.view_tag.is_some() {
|
|
||||||
return Err(ConsensusError::TransactionInvalidOutput(
|
|
||||||
"tagged output used before allowed",
|
|
||||||
));
|
|
||||||
} else if hf >= &HardFork::V16 && out.view_tag.is_none() {
|
|
||||||
return Err(ConsensusError::TransactionInvalidOutput(
|
|
||||||
"output does not have a view tag",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks that an output amount is decomposed.
|
|
||||||
///
|
|
||||||
/// This is also used during miner tx verification.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/pre_rct.html#output-amount
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#output-amounts
|
|
||||||
pub(crate) fn is_decomposed_amount(amount: u64) -> bool {
|
|
||||||
decomposed_amounts().binary_search(&amount).is_ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks the outputs amount for version 1 txs.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/pre_rct.html#output-amount
|
|
||||||
fn check_output_amount_v1(amount: u64, hf: &HardFork) -> Result<(), ConsensusError> {
|
|
||||||
if amount == 0 {
|
|
||||||
return Err(ConsensusError::TransactionInvalidOutput(
|
|
||||||
"zero amount output for v1 tx",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if hf >= &HardFork::V2 && !is_decomposed_amount(amount) {
|
|
||||||
return Err(ConsensusError::TransactionInvalidOutput(
|
|
||||||
"v1 tx does not have decomposed amount",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sums the outputs, checking for overflow and other consensus rules.
|
|
||||||
///
|
|
||||||
/// Should only be used on v1 transactions.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/pre_rct.html#inputs-and-outputs-must-not-overflow
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/pre_rct.html#output-amount
|
|
||||||
fn sum_outputs_v1(outputs: &[Output], hf: &HardFork) -> Result<u64, ConsensusError> {
|
|
||||||
let mut sum: u64 = 0;
|
|
||||||
|
|
||||||
for out in outputs {
|
|
||||||
let raw_amount = out.amount.unwrap_or(0);
|
|
||||||
|
|
||||||
check_output_amount_v1(raw_amount, hf)?;
|
|
||||||
|
|
||||||
sum = sum
|
|
||||||
.checked_add(raw_amount)
|
|
||||||
.ok_or(ConsensusError::TransactionOutputsOverflow)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(sum)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks the outputs against all output consensus rules, returning the sum of the output amounts.
|
|
||||||
pub fn check_outputs(
|
|
||||||
outputs: &[Output],
|
|
||||||
hf: &HardFork,
|
|
||||||
tx_version: &TxVersion,
|
|
||||||
) -> Result<u64, ConsensusError> {
|
|
||||||
check_output_types(outputs, hf)?;
|
|
||||||
check_output_keys(outputs)?;
|
|
||||||
|
|
||||||
match tx_version {
|
|
||||||
TxVersion::RingSignatures => sum_outputs_v1(outputs, hf),
|
|
||||||
_ => todo!("RingCT"),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
use monero_serai::transaction::Transaction;
|
|
||||||
|
|
||||||
use crate::{transactions::contextual_data::Rings, ConsensusError};
|
|
||||||
|
|
||||||
mod ring_sigs;
|
|
||||||
|
|
||||||
pub fn verify_signatures(tx: &Transaction, rings: &Rings) -> Result<(), ConsensusError> {
|
|
||||||
match rings {
|
|
||||||
Rings::Legacy(_) => ring_sigs::verify_inputs_signatures(
|
|
||||||
&tx.prefix.inputs,
|
|
||||||
&tx.signatures,
|
|
||||||
rings,
|
|
||||||
&tx.signature_hash(),
|
|
||||||
),
|
|
||||||
_ => panic!("TODO: RCT"),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
//! Version 1 ring signature verification.
|
|
||||||
//!
|
|
||||||
//! Some checks have to be done at deserialization or with data we don't have so we can't do them here, those checks are:
|
|
||||||
//! https://cuprate.github.io/monero-book/consensus_rules/transactions/pre_rct.html#signatures-must-be-canonical
|
|
||||||
//! this happens at deserialization in monero-serai.
|
|
||||||
//! https://cuprate.github.io/monero-book/consensus_rules/transactions/pre_rct.html#amount-of-signatures-in-a-ring
|
|
||||||
//! and this happens during ring signature verification in monero-serai.
|
|
||||||
//!
|
|
||||||
use monero_serai::{ring_signatures::RingSignature, transaction::Input};
|
|
||||||
use rayon::prelude::*;
|
|
||||||
|
|
||||||
use super::Rings;
|
|
||||||
use crate::ConsensusError;
|
|
||||||
|
|
||||||
/// Verifies the ring signature.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/pre_rct.html#the-ring-signature-must-be-valid
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/pre_rct.html#amount-of-ring-signatures
|
|
||||||
pub fn verify_inputs_signatures(
|
|
||||||
inputs: &[Input],
|
|
||||||
signatures: &[RingSignature],
|
|
||||||
rings: &Rings,
|
|
||||||
tx_sig_hash: &[u8; 32],
|
|
||||||
) -> Result<(), ConsensusError> {
|
|
||||||
match rings {
|
|
||||||
Rings::Legacy(rings) => {
|
|
||||||
// rings.len() != inputs.len() can't happen but check any way.
|
|
||||||
if signatures.len() != inputs.len() || rings.len() != inputs.len() {
|
|
||||||
return Err(ConsensusError::TransactionSignatureInvalid(
|
|
||||||
"number of ring sigs != inputs",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
inputs
|
|
||||||
.par_iter()
|
|
||||||
.zip(rings)
|
|
||||||
.zip(signatures)
|
|
||||||
.try_for_each(|((input, ring), sig)| {
|
|
||||||
let Input::ToKey { key_image, .. } = input else {
|
|
||||||
panic!("How did we build a ring with no decoys?");
|
|
||||||
};
|
|
||||||
|
|
||||||
if !sig.verify(tx_sig_hash, ring, key_image) {
|
|
||||||
return Err(ConsensusError::TransactionSignatureInvalid(
|
|
||||||
"Invalid ring signature",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
_ => panic!("tried to verify v1 tx with a non v1 ring"),
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
//! # Time Locks
|
|
||||||
//!
|
|
||||||
//! This module contains the checks for time locks, using the `check_all_time_locks` function.
|
|
||||||
//!
|
|
||||||
use monero_serai::transaction::Timelock;
|
|
||||||
|
|
||||||
use crate::{ConsensusError, HardFork};
|
|
||||||
|
|
||||||
/// Checks all the time locks are unlocked.
|
|
||||||
///
|
|
||||||
/// `current_time_lock_timestamp` must be: https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#getting-the-current-time
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#unlock-time
|
|
||||||
pub fn check_all_time_locks(
|
|
||||||
time_locks: &[Timelock],
|
|
||||||
current_chain_height: u64,
|
|
||||||
current_time_lock_timestamp: u64,
|
|
||||||
hf: &HardFork,
|
|
||||||
) -> Result<(), ConsensusError> {
|
|
||||||
time_locks.iter().try_for_each(|time_lock| {
|
|
||||||
if !output_unlocked(
|
|
||||||
time_lock,
|
|
||||||
current_chain_height,
|
|
||||||
current_time_lock_timestamp,
|
|
||||||
hf,
|
|
||||||
) {
|
|
||||||
Err(ConsensusError::TransactionHasInvalidRing(
|
|
||||||
"One or more ring members locked",
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if an outputs unlock time has passed.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#unlock-time
|
|
||||||
fn output_unlocked(
|
|
||||||
time_lock: &Timelock,
|
|
||||||
current_chain_height: u64,
|
|
||||||
current_time_lock_timestamp: u64,
|
|
||||||
hf: &HardFork,
|
|
||||||
) -> bool {
|
|
||||||
match *time_lock {
|
|
||||||
Timelock::None => true,
|
|
||||||
Timelock::Block(unlock_height) => {
|
|
||||||
check_block_time_lock(unlock_height.try_into().unwrap(), current_chain_height)
|
|
||||||
}
|
|
||||||
Timelock::Time(unlock_time) => {
|
|
||||||
check_timestamp_time_lock(unlock_time, current_time_lock_timestamp, hf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns if a locked output, which uses a block height, can be spend.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#block-height
|
|
||||||
fn check_block_time_lock(unlock_height: u64, current_chain_height: u64) -> bool {
|
|
||||||
// current_chain_height = 1 + top height
|
|
||||||
unlock_height <= current_chain_height
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ///
|
|
||||||
/// Returns if a locked output, which uses a block height, can be spend.
|
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#timestamp
|
|
||||||
fn check_timestamp_time_lock(
|
|
||||||
unlock_timestamp: u64,
|
|
||||||
current_time_lock_timestamp: u64,
|
|
||||||
hf: &HardFork,
|
|
||||||
) -> bool {
|
|
||||||
current_time_lock_timestamp + hf.block_time().as_secs() >= unlock_timestamp
|
|
||||||
}
|
|
Loading…
Reference in a new issue