mirror of
https://github.com/Cuprate/cuprate.git
synced 2024-12-22 19:49:28 +00:00
Split consensus rules into separate crate.
This commit is contained in:
parent
fbd324c45d
commit
3eea0b73bd
12 changed files with 1160 additions and 5 deletions
14
Cargo.toml
14
Cargo.toml
|
@ -4,6 +4,7 @@ resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"common",
|
"common",
|
||||||
"consensus",
|
"consensus",
|
||||||
|
"consensus/rules",
|
||||||
"cryptonight",
|
"cryptonight",
|
||||||
# "cuprate",
|
# "cuprate",
|
||||||
# "database",
|
# "database",
|
||||||
|
@ -39,14 +40,14 @@ bytes = { version = "1.5.0" }
|
||||||
clap = { version = "4.4.7" }
|
clap = { version = "4.4.7" }
|
||||||
chrono = { version = "0.4.31" }
|
chrono = { version = "0.4.31" }
|
||||||
crypto-bigint = { version = "0.5.3" }
|
crypto-bigint = { version = "0.5.3" }
|
||||||
curve25519-dalek = { version = "4.11" }
|
curve25519-dalek = { version = "4.1.1" }
|
||||||
dalek-ff-group = { git = "https://github.com/Cuprate/serai.git", rev = "39eafae" }
|
dalek-ff-group = { git = "https://github.com/Cuprate/serai.git", rev = "77edd00" }
|
||||||
dirs = { version = "5.0.1" }
|
dirs = { version = "5.0.1" }
|
||||||
futures = { version = "0.3.29" }
|
futures = { version = "0.3.29" }
|
||||||
hex = { version = "0.4.3" }
|
hex = { version = "0.4.3" }
|
||||||
monero-epee-bin-serde = { git = "https://github.com/monero-rs/monero-epee-bin-serde.git", rev = "e4a585a" }
|
monero-epee-bin-serde = { git = "https://github.com/monero-rs/monero-epee-bin-serde.git", rev = "e4a585a" }
|
||||||
monero-serai = { git = "https://github.com/Cuprate/serai.git", rev = "39eafae" }
|
monero-serai = { git = "https://github.com/Cuprate/serai.git", rev = "77edd00" }
|
||||||
multiexp = { git = "https://github.com/Cuprate/serai.git", rev = "39eafae" }
|
multiexp = { git = "https://github.com/Cuprate/serai.git", rev = "77edd00" }
|
||||||
randomx-rs = { version = "1.2.1" }
|
randomx-rs = { version = "1.2.1" }
|
||||||
rand = { version = "0.8.5" }
|
rand = { version = "0.8.5" }
|
||||||
rayon = { version = "1.8.0" }
|
rayon = { version = "1.8.0" }
|
||||||
|
@ -60,6 +61,11 @@ tower = { version = "0.4.13", features = ["util", "steer"] }
|
||||||
tracing-subscriber = { version = "0.3.17" }
|
tracing-subscriber = { version = "0.3.17" }
|
||||||
tracing = { version = "0.1.40" }
|
tracing = { version = "0.1.40" }
|
||||||
|
|
||||||
|
## workspace.dev-dependencies
|
||||||
|
proptest = { version = "1" }
|
||||||
|
proptest-derive = { version = "0.4.0" }
|
||||||
|
|
||||||
|
|
||||||
## TODO:
|
## TODO:
|
||||||
## Potential dependencies.
|
## Potential dependencies.
|
||||||
# arc-swap = { version = "1.6.0" } # Atomically swappable Arc<T> | https://github.com/vorner/arc-swap
|
# arc-swap = { version = "1.6.0" } # Atomically swappable Arc<T> | https://github.com/vorner/arc-swap
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "monero-consensus"
|
name = "cuprate-consensus"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "A crate implimenting all Moneros consensus rules."
|
description = "A crate implimenting all Moneros consensus rules."
|
||||||
|
|
10
consensus/README.md
Normal file
10
consensus/README.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# Consensus Rules
|
||||||
|
|
||||||
|
This folder contains 2 crates: `monero-consensus` (rules) and `cuprate-consensus`. `monero-consensus` contains the raw-rules
|
||||||
|
and isb built to be a more flexible library which requires the user to give the correct data and do minimal calculations, `cuprate-consensus`
|
||||||
|
on the other hand contains multiple tower::Services that handle tx/ block verification as a whole with a `context` service that
|
||||||
|
keeps track of blockchain state. `cuprate-consensus` uses `monero-consensus` internally.
|
||||||
|
|
||||||
|
If you are looking to use monero consensus rules it's recommended you try to integrate `cuprate-consensus` and fall back to
|
||||||
|
`monero-consensus` if you need more flexibility.
|
||||||
|
|
24
consensus/rules/Cargo.toml
Normal file
24
consensus/rules/Cargo.toml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
[package]
|
||||||
|
name = "monero-consensus"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
proptest = ["dep:proptest", "dep:proptest-derive"]
|
||||||
|
rayon = ["dep:rayon"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
monero-serai = { workspace = true }
|
||||||
|
curve25519-dalek = { workspace = true }
|
||||||
|
|
||||||
|
tracing = { workspace = true }
|
||||||
|
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
|
||||||
|
rayon = { workspace = true, optional = true }
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
proptest = {workspace = true, optional = true}
|
||||||
|
proptest-derive = {workspace = true, optional = true}
|
||||||
|
|
63
consensus/rules/src/decomposed_amount.rs
Normal file
63
consensus/rules/src/decomposed_amount.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
/// Decomposed amount table.
|
||||||
|
///
|
||||||
|
static DECOMPOSED_AMOUNTS: OnceLock<[u64; 172]> = OnceLock::new();
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub fn decomposed_amounts() -> &'static [u64; 172] {
|
||||||
|
DECOMPOSED_AMOUNTS.get_or_init(|| {
|
||||||
|
[
|
||||||
|
1, 2, 3, 4, 5, 6, 7, 8, 9,
|
||||||
|
10, 20, 30, 40, 50, 60, 70, 80, 90,
|
||||||
|
100, 200, 300, 400, 500, 600, 700, 800, 900,
|
||||||
|
1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000,
|
||||||
|
10000, 20000, 30000, 40000, 50000, 60000, 70000, 80000, 90000,
|
||||||
|
100000, 200000, 300000, 400000, 500000, 600000, 700000, 800000, 900000,
|
||||||
|
1000000, 2000000, 3000000, 4000000, 5000000, 6000000, 7000000, 8000000, 9000000,
|
||||||
|
10000000, 20000000, 30000000, 40000000, 50000000, 60000000, 70000000, 80000000, 90000000,
|
||||||
|
100000000, 200000000, 300000000, 400000000, 500000000, 600000000, 700000000, 800000000, 900000000,
|
||||||
|
1000000000, 2000000000, 3000000000, 4000000000, 5000000000, 6000000000, 7000000000, 8000000000, 9000000000,
|
||||||
|
10000000000, 20000000000, 30000000000, 40000000000, 50000000000, 60000000000, 70000000000, 80000000000, 90000000000,
|
||||||
|
100000000000, 200000000000, 300000000000, 400000000000, 500000000000, 600000000000, 700000000000, 800000000000, 900000000000,
|
||||||
|
1000000000000, 2000000000000, 3000000000000, 4000000000000, 5000000000000, 6000000000000, 7000000000000, 8000000000000, 9000000000000,
|
||||||
|
10000000000000, 20000000000000, 30000000000000, 40000000000000, 50000000000000, 60000000000000, 70000000000000, 80000000000000, 90000000000000,
|
||||||
|
100000000000000, 200000000000000, 300000000000000, 400000000000000, 500000000000000, 600000000000000, 700000000000000, 800000000000000, 900000000000000,
|
||||||
|
1000000000000000, 2000000000000000, 3000000000000000, 4000000000000000, 5000000000000000, 6000000000000000, 7000000000000000, 8000000000000000, 9000000000000000,
|
||||||
|
10000000000000000, 20000000000000000, 30000000000000000, 40000000000000000, 50000000000000000, 60000000000000000, 70000000000000000, 80000000000000000, 90000000000000000,
|
||||||
|
100000000000000000, 200000000000000000, 300000000000000000, 400000000000000000, 500000000000000000, 600000000000000000, 700000000000000000, 800000000000000000, 900000000000000000,
|
||||||
|
1000000000000000000, 2000000000000000000, 3000000000000000000, 4000000000000000000, 5000000000000000000, 6000000000000000000, 7000000000000000000, 8000000000000000000, 9000000000000000000,
|
||||||
|
10000000000000000000]
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
#[inline]
|
||||||
|
pub fn is_decomposed_amount(amount: &u64) -> bool {
|
||||||
|
decomposed_amounts().binary_search(amount).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decomposed_amounts_return_decomposed() {
|
||||||
|
for amount in decomposed_amounts() {
|
||||||
|
assert!(is_decomposed_amount(amount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decomposed_amounts_return_not_decomposed() {
|
||||||
|
assert!(!is_decomposed_amount(&21));
|
||||||
|
assert!(!is_decomposed_amount(&345431));
|
||||||
|
assert!(!is_decomposed_amount(&20000001));
|
||||||
|
}
|
||||||
|
}
|
270
consensus/rules/src/hard_forks.rs
Normal file
270
consensus/rules/src/hard_forks.rs
Normal file
|
@ -0,0 +1,270 @@
|
||||||
|
//! # Hard-Forks
|
||||||
|
//!
|
||||||
|
//! Monero use hard-forks to update it's protocol, this module contains a [`HardFork`] enum which is
|
||||||
|
//! an identifier for every current hard-fork.
|
||||||
|
//!
|
||||||
|
//! 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.
|
||||||
|
//!
|
||||||
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
|
fmt::{Display, Formatter},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Target block time for hf 1.
|
||||||
|
const BLOCK_TIME_V1: Duration = Duration::from_secs(60);
|
||||||
|
/// Target block time from v2.
|
||||||
|
const BLOCK_TIME_V2: Duration = Duration::from_secs(120);
|
||||||
|
|
||||||
|
const NUMB_OF_HARD_FORKS: usize = 16;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)]
|
||||||
|
pub enum HardForkError {
|
||||||
|
#[error("The hard-fork is unknown")]
|
||||||
|
HardForkUnknown,
|
||||||
|
#[error("The block is on an incorrect hard-fork")]
|
||||||
|
VersionIncorrect,
|
||||||
|
#[error("The block's vote is for a previous hard-fork")]
|
||||||
|
VoteTooLow,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about every hard-fork Monero has had.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct HFsInfo([HFInfo; NUMB_OF_HARD_FORKS]);
|
||||||
|
|
||||||
|
impl HFsInfo {
|
||||||
|
pub fn info_for_hf(&self, hf: &HardFork) -> HFInfo {
|
||||||
|
self.0[*hf as usize - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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() -> HFsInfo {
|
||||||
|
Self([
|
||||||
|
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),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An identifier for every hard-fork Monero has had.
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
|
||||||
|
#[cfg_attr(proptest, 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, HardForkError> {
|
||||||
|
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(HardForkError::HardForkUnknown),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 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(
|
||||||
|
current_hf: &Self,
|
||||||
|
version: &HardFork,
|
||||||
|
vote: &HardFork,
|
||||||
|
) -> Result<(), HardForkError> {
|
||||||
|
if current_hf != version {
|
||||||
|
Err(HardForkError::VersionIncorrect)?;
|
||||||
|
}
|
||||||
|
if current_hf < vote {
|
||||||
|
Err(HardForkError::VoteTooLow)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A struct holding the current voting state of the blockchain.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if a future hard fork should be activated, returning the next hard-fork that should be
|
||||||
|
/// activated.
|
||||||
|
///
|
||||||
|
/// https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#accepting-a-fork
|
||||||
|
pub fn check_next_hard_fork(
|
||||||
|
&self,
|
||||||
|
current_hf: &HardFork,
|
||||||
|
current_height: u64,
|
||||||
|
window: u64,
|
||||||
|
hfs_info: &HFsInfo,
|
||||||
|
) -> Option<HardFork> {
|
||||||
|
let mut approved_next_hf = None;
|
||||||
|
while let Some(next_hf) = current_hf.next_fork() {
|
||||||
|
let hf_info = hfs_info.info_for_hf(&next_hf);
|
||||||
|
if current_height >= hf_info.height
|
||||||
|
&& self.votes_for_hf(&next_hf) >= votes_needed(hf_info.threshold, window)
|
||||||
|
{
|
||||||
|
approved_next_hf = Some(next_hf);
|
||||||
|
} else {
|
||||||
|
// if we don't have enough votes for this fork any future fork won't have enough votes
|
||||||
|
// as votes are cumulative.
|
||||||
|
// TODO: If a future fork has a lower threshold that could not be true, but as all current forks
|
||||||
|
// have threshold 0 it is ok for now.
|
||||||
|
return approved_next_hf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
approved_next_hf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the votes needed for a hard-fork.
|
||||||
|
///
|
||||||
|
/// https://cuprate.github.io/monero-book/consensus_rules/hardforks.html#accepting-a-fork
|
||||||
|
pub fn votes_needed(threshold: u64, window: u64) -> u64 {
|
||||||
|
(threshold * window).div_ceil(100)
|
||||||
|
}
|
53
consensus/rules/src/lib.rs
Normal file
53
consensus/rules/src/lib.rs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
mod decomposed_amount;
|
||||||
|
mod hard_forks;
|
||||||
|
mod miner_tx;
|
||||||
|
mod signatures;
|
||||||
|
mod transactions;
|
||||||
|
|
||||||
|
pub use decomposed_amount::is_decomposed_amount;
|
||||||
|
pub use hard_forks::{HFVotes, HFsInfo, HardFork};
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks that a point is canonical.
|
||||||
|
///
|
||||||
|
/// https://github.com/dalek-cryptography/curve25519-dalek/issues/380
|
||||||
|
fn check_point(point: &curve25519_dalek::edwards::CompressedEdwardsY) -> bool {
|
||||||
|
let bytes = point.as_bytes();
|
||||||
|
|
||||||
|
point
|
||||||
|
.decompress()
|
||||||
|
// Ban points which are either unreduced or -0
|
||||||
|
.filter(|point| point.compress().as_bytes() == bytes)
|
||||||
|
.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "rayon")]
|
||||||
|
fn try_par_iter<T>(t: T) -> T::Iter
|
||||||
|
where
|
||||||
|
T: rayon::iter::IntoParallelIterator,
|
||||||
|
{
|
||||||
|
t.into_par_iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "rayon"))]
|
||||||
|
fn try_par_iter<T>(t: T) -> impl std::iter::Iterator<Item = T::Item>
|
||||||
|
where
|
||||||
|
T: std::iter::IntoIterator,
|
||||||
|
{
|
||||||
|
t.into_iter()
|
||||||
|
}
|
189
consensus/rules/src/miner_tx.rs
Normal file
189
consensus/rules/src/miner_tx.rs
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
use monero_serai::{
|
||||||
|
ringct::RctType,
|
||||||
|
transaction::{Input, Output, Timelock, Transaction},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{is_decomposed_amount, transactions::check_output_types, HardFork, TxVersion};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||||
|
pub enum MinerTxError {
|
||||||
|
#[error("The miners transaction version is invalid.")]
|
||||||
|
VersionInvalid,
|
||||||
|
#[error("The miner transaction does not have exactly one input.")]
|
||||||
|
IncorrectNumbOfInputs,
|
||||||
|
#[error("The miner transactions input has the wrong block height.")]
|
||||||
|
InputsHeightIncorrect,
|
||||||
|
#[error("The input is not of type `gen`.")]
|
||||||
|
InputNotOfTypeGen,
|
||||||
|
#[error("The transaction has an incorrect lock time.")]
|
||||||
|
InvalidLockTime,
|
||||||
|
#[error("The transaction has an output which is not decomposed.")]
|
||||||
|
OutputNotDecomposed,
|
||||||
|
#[error("The transaction outputs overflow when summed.")]
|
||||||
|
OutputsOverflow,
|
||||||
|
#[error("The miner transaction outputs the wrong amount.")]
|
||||||
|
OutputAmountIncorrect,
|
||||||
|
#[error("The miner transactions RCT type is not NULL.")]
|
||||||
|
RCTTypeNotNULL,
|
||||||
|
#[error("The miner transactions has an invalid output type.")]
|
||||||
|
InvalidOutputType,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A constant called "money supply", not actually a cap, it is used during
|
||||||
|
/// block reward calculations.
|
||||||
|
const MONEY_SUPPLY: u64 = u64::MAX;
|
||||||
|
/// The minimum block reward per minute, "tail-emission"
|
||||||
|
const MINIMUM_REWARD_PER_MIN: u64 = 3 * 10_u64.pow(11);
|
||||||
|
/// The value which `lock_time` should be for a coinbase output.
|
||||||
|
const MINER_TX_TIME_LOCKED_BLOCKS: u64 = 60;
|
||||||
|
|
||||||
|
/// Calculates the base block reward without taking away the penalty for expanding
|
||||||
|
/// the block.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the miner reward for this block.
|
||||||
|
pub fn calculate_block_reward(
|
||||||
|
block_weight: usize,
|
||||||
|
median_bw: usize,
|
||||||
|
already_generated_coins: u64,
|
||||||
|
hf: &HardFork,
|
||||||
|
) -> u64 {
|
||||||
|
let base_reward = calculate_base_reward(already_generated_coins, hf);
|
||||||
|
|
||||||
|
if block_weight <= median_bw {
|
||||||
|
return base_reward;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 as u128 * 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<(), MinerTxError> {
|
||||||
|
// The TxVersion enum checks if the version is not 1 or 2
|
||||||
|
if hf >= &HardFork::V12 && tx_version != &TxVersion::RingCT {
|
||||||
|
Err(MinerTxError::VersionInvalid)
|
||||||
|
} 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<(), MinerTxError> {
|
||||||
|
if inputs.len() != 1 {
|
||||||
|
return Err(MinerTxError::IncorrectNumbOfInputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
match &inputs[0] {
|
||||||
|
Input::Gen(height) => {
|
||||||
|
if height != &chain_height {
|
||||||
|
Err(MinerTxError::InputsHeightIncorrect)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(MinerTxError::InputNotOfTypeGen),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<(), MinerTxError> {
|
||||||
|
match time_lock {
|
||||||
|
Timelock::Block(till_height) => {
|
||||||
|
// Lock times above this amount are timestamps not blocks.
|
||||||
|
// This is just for safety though and shouldn't actually be hit.
|
||||||
|
if till_height >= &500_000_000 {
|
||||||
|
Err(MinerTxError::InvalidLockTime)?;
|
||||||
|
}
|
||||||
|
if u64::try_from(*till_height).unwrap() != chain_height + MINER_TX_TIME_LOCKED_BLOCKS {
|
||||||
|
Err(MinerTxError::InvalidLockTime)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(MinerTxError::InvalidLockTime),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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, MinerTxError> {
|
||||||
|
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(MinerTxError::OutputNotDecomposed);
|
||||||
|
}
|
||||||
|
sum = sum.checked_add(amt).ok_or(MinerTxError::OutputsOverflow)?;
|
||||||
|
}
|
||||||
|
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, MinerTxError> {
|
||||||
|
if hf == &HardFork::V1 || hf >= &HardFork::V12 {
|
||||||
|
if total_output != reward + fees {
|
||||||
|
return Err(MinerTxError::OutputAmountIncorrect);
|
||||||
|
}
|
||||||
|
Ok(reward)
|
||||||
|
} else {
|
||||||
|
if total_output - fees > reward || total_output > reward + fees {
|
||||||
|
return Err(MinerTxError::OutputAmountIncorrect);
|
||||||
|
}
|
||||||
|
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, MinerTxError> {
|
||||||
|
let tx_version = TxVersion::from_raw(tx.prefix.version).ok_or(MinerTxError::VersionInvalid)?;
|
||||||
|
check_miner_tx_version(&tx_version, hf)?;
|
||||||
|
|
||||||
|
if hf >= &HardFork::V12 && tx.rct_signatures.rct_type() != RctType::Null {
|
||||||
|
return Err(MinerTxError::RCTTypeNotNULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
check_time_lock(&tx.prefix.timelock, chain_height)?;
|
||||||
|
|
||||||
|
check_inputs(&tx.prefix.inputs, chain_height)?;
|
||||||
|
|
||||||
|
check_output_types(&tx.prefix.outputs, hf).map_err(|_| MinerTxError::InvalidOutputType)?;
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
33
consensus/rules/src/signatures.rs
Normal file
33
consensus/rules/src/signatures.rs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
use curve25519_dalek::EdwardsPoint;
|
||||||
|
use monero_serai::transaction::Transaction;
|
||||||
|
|
||||||
|
mod ring_signatures;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||||
|
pub enum SignatureError {
|
||||||
|
#[error("Number of signatures is different to the amount required.")]
|
||||||
|
MismatchSignatureSize,
|
||||||
|
#[error("The signature is incorrect.")]
|
||||||
|
IncorrectSignature,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the ring members of all the inputs.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Rings {
|
||||||
|
/// Legacy, pre-ringCT, rings.
|
||||||
|
Legacy(Vec<Vec<EdwardsPoint>>),
|
||||||
|
// RingCT rings, (outkey, amount commitment).
|
||||||
|
RingCT(Vec<Vec<[EdwardsPoint; 2]>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_contextual_signatures(tx: &Transaction, rings: &Rings) -> Result<(), SignatureError> {
|
||||||
|
match rings {
|
||||||
|
Rings::Legacy(_) => ring_signatures::verify_inputs_signatures(
|
||||||
|
&tx.prefix.inputs,
|
||||||
|
&tx.signatures,
|
||||||
|
rings,
|
||||||
|
&tx.signature_hash(),
|
||||||
|
),
|
||||||
|
_ => panic!("TODO: RCT"),
|
||||||
|
}
|
||||||
|
}
|
51
consensus/rules/src/signatures/ring_signatures.rs
Normal file
51
consensus/rules/src/signatures/ring_signatures.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
//! 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};
|
||||||
|
|
||||||
|
#[cfg(feature = "rayon")]
|
||||||
|
use rayon::prelude::*;
|
||||||
|
|
||||||
|
use super::{Rings, SignatureError};
|
||||||
|
use crate::par_iter;
|
||||||
|
|
||||||
|
/// 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<(), SignatureError> {
|
||||||
|
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(SignatureError::MismatchSignatureSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
par_iter(inputs)
|
||||||
|
.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(SignatureError::IncorrectSignature);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
_ => panic!("tried to verify v1 tx with a non v1 ring"),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
0
consensus/rules/src/time_locks.rs
Normal file
0
consensus/rules/src/time_locks.rs
Normal file
456
consensus/rules/src/transactions.rs
Normal file
456
consensus/rules/src/transactions.rs
Normal file
|
@ -0,0 +1,456 @@
|
||||||
|
use std::{cmp::Ordering, collections::HashSet, sync::Arc};
|
||||||
|
|
||||||
|
use monero_serai::transaction::{Input, Output, Timelock};
|
||||||
|
|
||||||
|
use crate::{check_point, is_decomposed_amount, HardFork, TxVersion};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||||
|
pub enum TransactionError {
|
||||||
|
//-------------------------------------------------------- OUTPUTS
|
||||||
|
#[error("Output is not a valid point.")]
|
||||||
|
OutputNotValidPoint,
|
||||||
|
#[error("The transaction has an invalid output type.")]
|
||||||
|
OutputTypeInvalid,
|
||||||
|
#[error("The transaction is v1 with a 0 amount output.")]
|
||||||
|
ZeroOutputForV1,
|
||||||
|
#[error("The transaction has an output which is not decomposed.")]
|
||||||
|
AmountNotDecomposed,
|
||||||
|
#[error("The transactions outputs overflow.")]
|
||||||
|
OutputsOverflow,
|
||||||
|
//-------------------------------------------------------- INPUTS
|
||||||
|
#[error("One or more inputs don't have the expected number of decoys.")]
|
||||||
|
InputDoesNotHaveExpectedNumbDecoys,
|
||||||
|
#[error("The transaction has more than one mixable input with unmixable inputs.")]
|
||||||
|
MoreThanOneMixableInputWithUnmixable,
|
||||||
|
#[error("The key-image is not in the prime sub-group.")]
|
||||||
|
KeyImageIsNotInPrimeSubGroup,
|
||||||
|
#[error("Key-image is already spent.")]
|
||||||
|
KeyImageSpent,
|
||||||
|
#[error("The input is not the expected type.")]
|
||||||
|
IncorrectInputType,
|
||||||
|
#[error("The transaction has a duplicate ring member.")]
|
||||||
|
DuplicateRingMember,
|
||||||
|
#[error("The transaction inputs are not ordered.")]
|
||||||
|
InputsAreNotOrdered,
|
||||||
|
#[error("The transaction spends a decoy which is too young.")]
|
||||||
|
OneOrMoreDecoysLocked,
|
||||||
|
#[error("The transaction inputs overflow.")]
|
||||||
|
InputsOverflow,
|
||||||
|
#[error("The transaction has no inputs.")]
|
||||||
|
NoInputs,
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------------------------------------- OUTPUTS
|
||||||
|
|
||||||
|
/// 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<(), TransactionError> {
|
||||||
|
for out in outputs {
|
||||||
|
if !check_point(&out.key) {
|
||||||
|
return Err(TransactionError::OutputNotValidPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<(), TransactionError> {
|
||||||
|
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(TransactionError::OutputTypeInvalid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
for out in outputs {
|
||||||
|
if hf <= &HardFork::V14 && out.view_tag.is_some()
|
||||||
|
|| hf >= &HardFork::V16 && out.view_tag.is_none()
|
||||||
|
{
|
||||||
|
return Err(TransactionError::OutputTypeInvalid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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<(), TransactionError> {
|
||||||
|
if amount == 0 {
|
||||||
|
return Err(TransactionError::ZeroOutputForV1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if hf >= &HardFork::V2 && !is_decomposed_amount(&amount) {
|
||||||
|
return Err(TransactionError::AmountNotDecomposed);
|
||||||
|
}
|
||||||
|
|
||||||
|
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, TransactionError> {
|
||||||
|
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(TransactionError::OutputsOverflow)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, TransactionError> {
|
||||||
|
check_output_types(outputs, hf)?;
|
||||||
|
check_output_keys(outputs)?;
|
||||||
|
|
||||||
|
match tx_version {
|
||||||
|
TxVersion::RingSignatures => sum_outputs_v1(outputs, hf),
|
||||||
|
_ => todo!("RingCT"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------------------------------------- INPUTS
|
||||||
|
|
||||||
|
/// 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 (RCT)
|
||||||
|
/// - The top block hash is the same as when this data was retrieved (the blockchain state is unchanged).
|
||||||
|
///
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the default 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
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<(), TransactionError> {
|
||||||
|
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 {
|
||||||
|
// Only allow rings without enough decoys if there aren't enough decoys to mix with.
|
||||||
|
if decoy_info.not_mixable == 0 {
|
||||||
|
return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
|
||||||
|
}
|
||||||
|
// Only allow upto 1 mixable input with unmixable inputs.
|
||||||
|
if decoy_info.mixable > 1 {
|
||||||
|
return Err(TransactionError::MoreThanOneMixableInputWithUnmixable);
|
||||||
|
}
|
||||||
|
} else if hf >= &HardFork::V8 && decoy_info.min_decoys != current_minimum_decoys {
|
||||||
|
// From V8 enforce the minimum used number of rings is the default minimum.
|
||||||
|
return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
|
||||||
|
}
|
||||||
|
|
||||||
|
// From v12 all inputs must have the same number of decoys.
|
||||||
|
if hf >= &HardFork::V12 && decoy_info.min_decoys != decoy_info.max_decoys {
|
||||||
|
return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<(), TransactionError> {
|
||||||
|
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(TransactionError::KeyImageIsNotInPrimeSubGroup);
|
||||||
|
}
|
||||||
|
if !spent_kis.insert(key_image.compress().to_bytes()) {
|
||||||
|
return Err(TransactionError::KeyImageSpent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(TransactionError::IncorrectInputType)?,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<(), TransactionError> {
|
||||||
|
match input {
|
||||||
|
Input::ToKey { .. } => Ok(()),
|
||||||
|
_ => Err(TransactionError::IncorrectInputType)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<(), TransactionError> {
|
||||||
|
match input {
|
||||||
|
Input::ToKey { key_offsets, .. } => {
|
||||||
|
if key_offsets.is_empty() {
|
||||||
|
Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(TransactionError::IncorrectInputType)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<(), TransactionError> {
|
||||||
|
if hf >= &HardFork::V6 {
|
||||||
|
match input {
|
||||||
|
Input::ToKey { key_offsets, .. } => key_offsets.iter().skip(1).try_for_each(|offset| {
|
||||||
|
if *offset == 0 {
|
||||||
|
Err(TransactionError::DuplicateRingMember)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
_ => Err(TransactionError::IncorrectInputType)?,
|
||||||
|
}
|
||||||
|
} 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<(), TransactionError> {
|
||||||
|
let get_ki = |inp: &Input| match inp {
|
||||||
|
Input::ToKey { key_image, .. } => Ok(key_image.compress().to_bytes()),
|
||||||
|
_ => Err(TransactionError::IncorrectInputType),
|
||||||
|
};
|
||||||
|
|
||||||
|
if hf >= &HardFork::V7 {
|
||||||
|
for inps in inputs.windows(2) {
|
||||||
|
match get_ki(&inps[0])?.cmp(&get_ki(&inps[1])?) {
|
||||||
|
Ordering::Less => (),
|
||||||
|
_ => return Err(TransactionError::InputsAreNotOrdered),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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(
|
||||||
|
youngest_used_out_height: u64,
|
||||||
|
current_chain_height: u64,
|
||||||
|
hf: &HardFork,
|
||||||
|
) -> Result<(), TransactionError> {
|
||||||
|
if hf >= &HardFork::V12 {
|
||||||
|
if youngest_used_out_height + 10 > current_chain_height {
|
||||||
|
Err(TransactionError::OneOrMoreDecoysLocked)
|
||||||
|
} 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, TransactionError> {
|
||||||
|
let mut sum: u64 = 0;
|
||||||
|
for inp in inputs {
|
||||||
|
match inp {
|
||||||
|
Input::ToKey { amount, .. } => {
|
||||||
|
sum = sum
|
||||||
|
.checked_add(amount.unwrap_or(0))
|
||||||
|
.ok_or(TransactionError::InputsOverflow)?;
|
||||||
|
}
|
||||||
|
_ => Err(TransactionError::IncorrectInputType)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks all input consensus rules.
|
||||||
|
///
|
||||||
|
/// TODO: list rules.
|
||||||
|
///
|
||||||
|
pub fn check_inputs(
|
||||||
|
inputs: &[Input],
|
||||||
|
youngest_used_out_height: u64,
|
||||||
|
current_chain_height: u64,
|
||||||
|
decoys_info: Option<&DecoyInfo>,
|
||||||
|
hf: &HardFork,
|
||||||
|
tx_version: &TxVersion,
|
||||||
|
spent_kis: Arc<std::sync::Mutex<HashSet<[u8; 32]>>>,
|
||||||
|
) -> Result<u64, TransactionError> {
|
||||||
|
if inputs.is_empty() {
|
||||||
|
return Err(TransactionError::NoInputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
check_10_block_lock(youngest_used_out_height, current_chain_height, hf)?;
|
||||||
|
|
||||||
|
if let Some(decoys_info) = decoys_info {
|
||||||
|
check_decoy_info(decoys_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)?;
|
||||||
|
// Adding this here for clarity so we don't add more work here while the mutex guard is still
|
||||||
|
// in scope.
|
||||||
|
drop(spent_kis_lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
check_inputs_sorted(inputs, hf)?;
|
||||||
|
|
||||||
|
match tx_version {
|
||||||
|
TxVersion::RingSignatures => sum_inputs_v1(inputs),
|
||||||
|
_ => panic!("TODO: RCT"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------------------------------------- TIME LOCKS
|
||||||
|
|
||||||
|
/// 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<(), TransactionError> {
|
||||||
|
time_locks.iter().try_for_each(|time_lock| {
|
||||||
|
if !output_unlocked(
|
||||||
|
time_lock,
|
||||||
|
current_chain_height,
|
||||||
|
current_time_lock_timestamp,
|
||||||
|
hf,
|
||||||
|
) {
|
||||||
|
Err(TransactionError::OneOrMoreDecoysLocked)
|
||||||
|
} 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