mirror of
https://github.com/Cuprate/cuprate.git
synced 2025-01-03 17:39:50 +00:00
finish rules for v1 txs - clean up is needed
This commit is contained in:
parent
9b7f778f60
commit
b727062e97
12 changed files with 365 additions and 291 deletions
|
@ -1,17 +1,11 @@
|
||||||
#![cfg(feature = "binaries")]
|
#![cfg(feature = "binaries")]
|
||||||
|
|
||||||
use futures::Sink;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fmt::{Display, Formatter};
|
|
||||||
use std::io::Read;
|
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use rayon::prelude::*;
|
|
||||||
use tower::{Service, ServiceExt};
|
use tower::{Service, ServiceExt};
|
||||||
use tracing::instrument;
|
|
||||||
use tracing::level_filters::LevelFilter;
|
use tracing::level_filters::LevelFilter;
|
||||||
|
|
||||||
use cuprate_common::Network;
|
use cuprate_common::Network;
|
||||||
|
@ -53,7 +47,8 @@ where
|
||||||
{
|
{
|
||||||
tracing::info!("Beginning chain scan");
|
tracing::info!("Beginning chain scan");
|
||||||
|
|
||||||
let chain_height = 3_000_000;
|
// TODO: when we implement all rules use the RPCs chain height, for now we don't check v2 txs.
|
||||||
|
let chain_height = 1288616;
|
||||||
|
|
||||||
tracing::info!("scanning to chain height: {}", chain_height);
|
tracing::info!("scanning to chain height: {}", chain_height);
|
||||||
|
|
||||||
|
@ -79,10 +74,6 @@ where
|
||||||
let mut current_height = start_height;
|
let mut current_height = start_height;
|
||||||
let mut next_batch_start_height = start_height + batch_size;
|
let mut next_batch_start_height = start_height + batch_size;
|
||||||
|
|
||||||
let mut time_to_verify_last_batch: u128 = 0;
|
|
||||||
|
|
||||||
let mut batches_till_check_batch_size: u64 = 2;
|
|
||||||
|
|
||||||
while next_batch_start_height < chain_height {
|
while next_batch_start_height < chain_height {
|
||||||
let next_batch_size = rpc_config.read().unwrap().block_batch_size();
|
let next_batch_size = rpc_config.read().unwrap().block_batch_size();
|
||||||
|
|
||||||
|
@ -96,56 +87,10 @@ where
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
|
|
||||||
let (DatabaseResponse::BlockBatchInRange(blocks), time_to_retrieve_batch) =
|
let (DatabaseResponse::BlockBatchInRange(blocks), _) = current_fut.await?? else {
|
||||||
current_fut.await??
|
|
||||||
else {
|
|
||||||
panic!("Database sent incorrect response!");
|
panic!("Database sent incorrect response!");
|
||||||
};
|
};
|
||||||
|
|
||||||
let time_to_verify_batch = std::time::Instant::now();
|
|
||||||
|
|
||||||
let time_to_retrieve_batch = time_to_retrieve_batch.as_millis();
|
|
||||||
/*
|
|
||||||
if time_to_retrieve_batch > time_to_verify_last_batch + 2000
|
|
||||||
&& batches_till_check_batch_size == 0
|
|
||||||
{
|
|
||||||
batches_till_check_batch_size = 3;
|
|
||||||
|
|
||||||
let mut conf = rpc_config.write().unwrap();
|
|
||||||
tracing::info!(
|
|
||||||
"Decreasing batch size time to verify last batch: {}, time_to_retrieve_batch: {}",
|
|
||||||
time_to_verify_last_batch,
|
|
||||||
time_to_retrieve_batch
|
|
||||||
);
|
|
||||||
conf.max_blocks_per_node = (conf.max_blocks_per_node
|
|
||||||
* time_to_verify_last_batch as u64
|
|
||||||
/ (time_to_retrieve_batch as u64))
|
|
||||||
.max(10_u64)
|
|
||||||
.min(MAX_BLOCKS_IN_RANGE);
|
|
||||||
tracing::info!("Decreasing batch size to: {}", conf.max_blocks_per_node);
|
|
||||||
} else if time_to_retrieve_batch + 2000 < time_to_verify_last_batch
|
|
||||||
&& batches_till_check_batch_size == 0
|
|
||||||
{
|
|
||||||
batches_till_check_batch_size = 3;
|
|
||||||
|
|
||||||
let mut conf = rpc_config.write().unwrap();
|
|
||||||
tracing::info!(
|
|
||||||
"Increasing batch size time to verify last batch: {}, time_to_retrieve_batch: {}",
|
|
||||||
time_to_verify_last_batch,
|
|
||||||
time_to_retrieve_batch
|
|
||||||
);
|
|
||||||
conf.max_blocks_per_node = (conf.max_blocks_per_node
|
|
||||||
* (time_to_verify_last_batch as u64)
|
|
||||||
/ time_to_retrieve_batch.max(1) as u64)
|
|
||||||
.max(30_u64)
|
|
||||||
.min(MAX_BLOCKS_IN_RANGE);
|
|
||||||
tracing::info!("Increasing batch size to: {}", conf.max_blocks_per_node);
|
|
||||||
} else {
|
|
||||||
batches_till_check_batch_size = batches_till_check_batch_size.saturating_sub(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Handling batch: {:?}, chain height: {}",
|
"Handling batch: {:?}, chain height: {}",
|
||||||
current_height..(current_height + blocks.len() as u64),
|
current_height..(current_height + blocks.len() as u64),
|
||||||
|
@ -190,8 +135,6 @@ where
|
||||||
cache.write().unwrap().save(&save_file)?;
|
cache.write().unwrap().save(&save_file)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
time_to_verify_last_batch = time_to_verify_batch.elapsed().as_millis();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -200,7 +143,7 @@ where
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_max_level(LevelFilter::DEBUG)
|
.with_max_level(LevelFilter::INFO)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let network = Network::Mainnet;
|
let network = Network::Mainnet;
|
||||||
|
|
|
@ -132,6 +132,7 @@ where
|
||||||
.oneshot(VerifyTxRequest::BatchSetupVerifyBlock {
|
.oneshot(VerifyTxRequest::BatchSetupVerifyBlock {
|
||||||
txs,
|
txs,
|
||||||
current_chain_height: context.chain_height,
|
current_chain_height: context.chain_height,
|
||||||
|
time_for_time_lock: context.current_adjusted_timestamp_for_time_lock(),
|
||||||
hf: context.current_hard_fork,
|
hf: context.current_hard_fork,
|
||||||
})
|
})
|
||||||
.await?
|
.await?
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
//!
|
//!
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
cmp::min,
|
||||||
future::Future,
|
future::Future,
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
|
@ -17,7 +18,7 @@ use futures::FutureExt;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use tower::{Service, ServiceExt};
|
use tower::{Service, ServiceExt};
|
||||||
|
|
||||||
use crate::{ConsensusError, Database, DatabaseRequest, DatabaseResponse};
|
use crate::{helper::current_time, ConsensusError, Database, DatabaseRequest, DatabaseResponse};
|
||||||
|
|
||||||
pub mod difficulty;
|
pub mod difficulty;
|
||||||
mod hardforks;
|
mod hardforks;
|
||||||
|
@ -27,7 +28,7 @@ pub use difficulty::DifficultyCacheConfig;
|
||||||
pub use hardforks::{HardFork, HardForkConfig};
|
pub use hardforks::{HardFork, HardForkConfig};
|
||||||
pub use weight::BlockWeightsCacheConfig;
|
pub use weight::BlockWeightsCacheConfig;
|
||||||
|
|
||||||
const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: usize = 60;
|
const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: u64 = 60;
|
||||||
|
|
||||||
pub struct ContextConfig {
|
pub struct ContextConfig {
|
||||||
hard_fork_cfg: HardForkConfig,
|
hard_fork_cfg: HardForkConfig,
|
||||||
|
@ -143,11 +144,10 @@ pub struct BlockChainContext {
|
||||||
pub median_weight_for_block_reward: usize,
|
pub median_weight_for_block_reward: usize,
|
||||||
/// The amount of coins minted already.
|
/// The amount of coins minted already.
|
||||||
pub already_generated_coins: u64,
|
pub already_generated_coins: u64,
|
||||||
/// Timestamp to use to check time locked outputs.
|
|
||||||
pub time_lock_timestamp: u64,
|
|
||||||
/// The median timestamp over the last [`BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW`] blocks, will be None if there aren't
|
/// The median timestamp over the last [`BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW`] blocks, will be None if there aren't
|
||||||
/// [`BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW`] blocks.
|
/// [`BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW`] blocks.
|
||||||
pub median_block_timestamp: Option<u64>,
|
pub median_block_timestamp: Option<u64>,
|
||||||
|
top_block_timestamp: Option<u64>,
|
||||||
/// The height of the chain.
|
/// The height of the chain.
|
||||||
pub chain_height: u64,
|
pub chain_height: u64,
|
||||||
/// The top blocks hash
|
/// The top blocks hash
|
||||||
|
@ -157,6 +157,29 @@ pub struct BlockChainContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BlockChainContext {
|
impl BlockChainContext {
|
||||||
|
/// Returns the timestamp the should be used when checking locked outputs.
|
||||||
|
///
|
||||||
|
/// 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 {
|
||||||
|
if self.current_hard_fork < HardFork::V13 || self.median_block_timestamp.is_none() {
|
||||||
|
current_time()
|
||||||
|
} else {
|
||||||
|
// This is safe as we just checked if this was None.
|
||||||
|
let median = self.median_block_timestamp.unwrap();
|
||||||
|
|
||||||
|
let adjusted_median = median
|
||||||
|
+ (BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW + 1)
|
||||||
|
* self.current_hard_fork.block_time().as_secs()
|
||||||
|
/ 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.
|
||||||
|
let adjusted_top_block =
|
||||||
|
self.top_block_timestamp.unwrap() + self.current_hard_fork.block_time().as_secs();
|
||||||
|
|
||||||
|
min(adjusted_median, adjusted_top_block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn block_blob_size_limit(&self) -> usize {
|
pub fn block_blob_size_limit(&self) -> usize {
|
||||||
self.effective_median_weight * 2 - 600
|
self.effective_median_weight * 2 - 600
|
||||||
}
|
}
|
||||||
|
@ -227,9 +250,9 @@ impl Service<BlockChainContextRequest> for BlockChainContextService {
|
||||||
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),
|
median_weight_for_block_reward: weight_cache.median_for_block_reward(¤t_hf),
|
||||||
already_generated_coins: *already_generated_coins,
|
already_generated_coins: *already_generated_coins,
|
||||||
time_lock_timestamp: 0, //TODO:
|
top_block_timestamp: difficulty_cache.top_block_timestamp(),
|
||||||
median_block_timestamp: difficulty_cache
|
median_block_timestamp: difficulty_cache
|
||||||
.median_timestamp(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW),
|
.median_timestamp(usize::try_from(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW).unwrap()),
|
||||||
chain_height: *chain_height,
|
chain_height: *chain_height,
|
||||||
top_hash: *top_block_hash,
|
top_hash: *top_block_hash,
|
||||||
current_hard_fork: current_hf,
|
current_hard_fork: current_hf,
|
||||||
|
|
|
@ -31,18 +31,22 @@ where
|
||||||
D: Database + Clone + Send + Sync + 'static,
|
D: Database + Clone + Send + Sync + 'static,
|
||||||
D::Future: Send + 'static,
|
D::Future: Send + 'static,
|
||||||
{
|
{
|
||||||
let (context_svc, context_svc_updater) = context::initialize_blockchain_context(cfg, database.clone()).await?;
|
let (context_svc, context_svc_updater) =
|
||||||
|
context::initialize_blockchain_context(cfg, database.clone()).await?;
|
||||||
let tx_svc = transactions::TxVerifierService::new(database);
|
let tx_svc = transactions::TxVerifierService::new(database);
|
||||||
let block_svc = block::BlockVerifierService::new(context_svc.clone(), tx_svc.clone());
|
let block_svc = block::BlockVerifierService::new(context_svc.clone(), tx_svc.clone());
|
||||||
Ok((block_svc, tx_svc, context_svc_updater))
|
Ok((block_svc, tx_svc, context_svc_updater))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: split this enum up.
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum ConsensusError {
|
pub enum ConsensusError {
|
||||||
#[error("Miner transaction invalid: {0}")]
|
#[error("Miner transaction invalid: {0}")]
|
||||||
MinerTransaction(&'static str),
|
MinerTransaction(&'static str),
|
||||||
#[error("Transaction sig invalid: {0}")]
|
#[error("Transaction sig invalid: {0}")]
|
||||||
TransactionSignatureInvalid(&'static str),
|
TransactionSignatureInvalid(&'static str),
|
||||||
|
#[error("Transaction has too high output amount")]
|
||||||
|
TransactionOutputsTooMuch,
|
||||||
#[error("Transaction inputs overflow")]
|
#[error("Transaction inputs overflow")]
|
||||||
TransactionInputsOverflow,
|
TransactionInputsOverflow,
|
||||||
#[error("Transaction outputs overflow")]
|
#[error("Transaction outputs overflow")]
|
||||||
|
@ -112,6 +116,8 @@ pub enum DatabaseRequest {
|
||||||
Outputs(HashMap<u64, HashSet<u64>>),
|
Outputs(HashMap<u64, HashSet<u64>>),
|
||||||
NumberOutputsWithAmount(u64),
|
NumberOutputsWithAmount(u64),
|
||||||
|
|
||||||
|
CheckKIsNotSpent(HashSet<[u8; 32]>),
|
||||||
|
|
||||||
#[cfg(feature = "binaries")]
|
#[cfg(feature = "binaries")]
|
||||||
BlockBatchInRange(std::ops::Range<u64>),
|
BlockBatchInRange(std::ops::Range<u64>),
|
||||||
}
|
}
|
||||||
|
@ -129,6 +135,8 @@ pub enum DatabaseResponse {
|
||||||
Outputs(HashMap<u64, HashMap<u64, OutputOnChain>>),
|
Outputs(HashMap<u64, HashMap<u64, OutputOnChain>>),
|
||||||
NumberOutputsWithAmount(usize),
|
NumberOutputsWithAmount(usize),
|
||||||
|
|
||||||
|
CheckKIsNotSpent(bool),
|
||||||
|
|
||||||
#[cfg(feature = "binaries")]
|
#[cfg(feature = "binaries")]
|
||||||
BlockBatchInRange(
|
BlockBatchInRange(
|
||||||
Vec<(
|
Vec<(
|
||||||
|
|
|
@ -295,7 +295,13 @@ impl<R: RpcConnection + Send + Sync + 'static> tower::Service<DatabaseRequest> f
|
||||||
}
|
}
|
||||||
.instrument(span)
|
.instrument(span)
|
||||||
.boxed(),
|
.boxed(),
|
||||||
|
DatabaseRequest::CheckKIsNotSpent(kis) => async move {
|
||||||
|
Ok(DatabaseResponse::CheckKIsNotSpent(
|
||||||
|
cache.read().unwrap().are_kis_spent(kis),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
.instrument(span)
|
||||||
|
.boxed(),
|
||||||
DatabaseRequest::GeneratedCoins => async move {
|
DatabaseRequest::GeneratedCoins => async move {
|
||||||
Ok(DatabaseResponse::GeneratedCoins(
|
Ok(DatabaseResponse::GeneratedCoins(
|
||||||
cache.read().unwrap().already_generated_coins,
|
cache.read().unwrap().already_generated_coins,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -7,7 +8,7 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use bincode::{Decode, Encode};
|
use bincode::{Decode, Encode};
|
||||||
use monero_serai::transaction::{Timelock, Transaction};
|
use monero_serai::transaction::{Input, Timelock, Transaction};
|
||||||
use tracing_subscriber::fmt::MakeWriter;
|
use tracing_subscriber::fmt::MakeWriter;
|
||||||
|
|
||||||
use cuprate_common::Network;
|
use cuprate_common::Network;
|
||||||
|
@ -24,6 +25,7 @@ pub struct ScanningCache {
|
||||||
// network: u8,
|
// network: u8,
|
||||||
numb_outs: HashMap<u64, u64>,
|
numb_outs: HashMap<u64, u64>,
|
||||||
time_locked_out: HashMap<[u8; 32], u64>,
|
time_locked_out: HashMap<[u8; 32], u64>,
|
||||||
|
kis: HashSet<[u8; 32]>,
|
||||||
pub already_generated_coins: u64,
|
pub already_generated_coins: u64,
|
||||||
/// The height of the *next* block to scan.
|
/// The height of the *next* block to scan.
|
||||||
pub height: u64,
|
pub height: u64,
|
||||||
|
@ -67,12 +69,23 @@ impl ScanningCache {
|
||||||
.outputs
|
.outputs
|
||||||
.iter()
|
.iter()
|
||||||
.for_each(|out| self.add_outs(out.amount.unwrap_or(0), 1));
|
.for_each(|out| self.add_outs(out.amount.unwrap_or(0), 1));
|
||||||
|
|
||||||
|
tx.tx.prefix.inputs.iter().for_each(|inp| match inp {
|
||||||
|
Input::ToKey { key_image, .. } => {
|
||||||
|
assert!(self.kis.insert(key_image.compress().to_bytes()))
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
self.already_generated_coins = self.already_generated_coins.saturating_add(generated_coins);
|
self.already_generated_coins = self.already_generated_coins.saturating_add(generated_coins);
|
||||||
self.height += 1;
|
self.height += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn are_kis_spent(&self, kis: HashSet<[u8; 32]>) -> bool {
|
||||||
|
self.kis.is_disjoint(&kis)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn outputs_time_lock(&self, tx: &[u8; 32]) -> Timelock {
|
pub fn outputs_time_lock(&self, tx: &[u8; 32]) -> Timelock {
|
||||||
let time_lock = self.time_locked_out.get(tx).copied().unwrap_or(0);
|
let time_lock = self.time_locked_out.get(tx).copied().unwrap_or(0);
|
||||||
match time_lock {
|
match time_lock {
|
||||||
|
|
|
@ -19,7 +19,7 @@ mod inputs;
|
||||||
pub(crate) mod outputs;
|
pub(crate) mod outputs;
|
||||||
mod ring;
|
mod ring;
|
||||||
mod sigs;
|
mod sigs;
|
||||||
//mod time_lock;
|
mod time_lock;
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
pub enum TxVersion {
|
pub enum TxVersion {
|
||||||
|
@ -71,6 +71,7 @@ pub enum VerifyTxRequest {
|
||||||
Block {
|
Block {
|
||||||
txs: Vec<Arc<TransactionVerificationData>>,
|
txs: Vec<Arc<TransactionVerificationData>>,
|
||||||
current_chain_height: u64,
|
current_chain_height: u64,
|
||||||
|
time_for_time_lock: u64,
|
||||||
hf: HardFork,
|
hf: HardFork,
|
||||||
},
|
},
|
||||||
/// Batches the setup of [`TransactionVerificationData`] and verifies the transactions
|
/// Batches the setup of [`TransactionVerificationData`] and verifies the transactions
|
||||||
|
@ -78,6 +79,7 @@ pub enum VerifyTxRequest {
|
||||||
BatchSetupVerifyBlock {
|
BatchSetupVerifyBlock {
|
||||||
txs: Vec<Transaction>,
|
txs: Vec<Transaction>,
|
||||||
current_chain_height: u64,
|
current_chain_height: u64,
|
||||||
|
time_for_time_lock: u64,
|
||||||
hf: HardFork,
|
hf: HardFork,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -123,13 +125,28 @@ where
|
||||||
VerifyTxRequest::Block {
|
VerifyTxRequest::Block {
|
||||||
txs,
|
txs,
|
||||||
current_chain_height,
|
current_chain_height,
|
||||||
|
time_for_time_lock,
|
||||||
hf,
|
hf,
|
||||||
} => verify_transactions_for_block(database, txs, current_chain_height, hf).boxed(),
|
} => verify_transactions_for_block(
|
||||||
|
database,
|
||||||
|
txs,
|
||||||
|
current_chain_height,
|
||||||
|
time_for_time_lock,
|
||||||
|
hf,
|
||||||
|
)
|
||||||
|
.boxed(),
|
||||||
VerifyTxRequest::BatchSetupVerifyBlock {
|
VerifyTxRequest::BatchSetupVerifyBlock {
|
||||||
txs,
|
txs,
|
||||||
current_chain_height,
|
current_chain_height,
|
||||||
|
time_for_time_lock,
|
||||||
hf,
|
hf,
|
||||||
} => batch_setup_verify_transactions_for_block(database, txs, current_chain_height, hf)
|
} => batch_setup_verify_transactions_for_block(
|
||||||
|
database,
|
||||||
|
txs,
|
||||||
|
current_chain_height,
|
||||||
|
time_for_time_lock,
|
||||||
|
hf,
|
||||||
|
)
|
||||||
.boxed(),
|
.boxed(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -166,6 +183,7 @@ async fn batch_setup_verify_transactions_for_block<D>(
|
||||||
database: D,
|
database: D,
|
||||||
txs: Vec<Transaction>,
|
txs: Vec<Transaction>,
|
||||||
current_chain_height: u64,
|
current_chain_height: u64,
|
||||||
|
time_for_time_lock: u64,
|
||||||
hf: HardFork,
|
hf: HardFork,
|
||||||
) -> Result<VerifyTxResponse, ConsensusError>
|
) -> Result<VerifyTxResponse, ConsensusError>
|
||||||
where
|
where
|
||||||
|
@ -180,7 +198,14 @@ where
|
||||||
.await
|
.await
|
||||||
.unwrap()?;
|
.unwrap()?;
|
||||||
|
|
||||||
verify_transactions_for_block(database, txs.clone(), current_chain_height, hf).await?;
|
verify_transactions_for_block(
|
||||||
|
database,
|
||||||
|
txs.clone(),
|
||||||
|
current_chain_height,
|
||||||
|
time_for_time_lock,
|
||||||
|
hf,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(VerifyTxResponse::BatchSetupOk(txs))
|
Ok(VerifyTxResponse::BatchSetupOk(txs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,6 +214,7 @@ async fn verify_transactions_for_block<D>(
|
||||||
database: D,
|
database: D,
|
||||||
txs: Vec<Arc<TransactionVerificationData>>,
|
txs: Vec<Arc<TransactionVerificationData>>,
|
||||||
current_chain_height: u64,
|
current_chain_height: u64,
|
||||||
|
time_for_time_lock: u64,
|
||||||
hf: HardFork,
|
hf: HardFork,
|
||||||
) -> Result<VerifyTxResponse, ConsensusError>
|
) -> Result<VerifyTxResponse, ConsensusError>
|
||||||
where
|
where
|
||||||
|
@ -202,7 +228,13 @@ where
|
||||||
|
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
txs.par_iter().try_for_each(|tx| {
|
txs.par_iter().try_for_each(|tx| {
|
||||||
verify_transaction_for_block(tx, current_chain_height, hf, spent_kis.clone())
|
verify_transaction_for_block(
|
||||||
|
tx,
|
||||||
|
current_chain_height,
|
||||||
|
time_for_time_lock,
|
||||||
|
hf,
|
||||||
|
spent_kis.clone(),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -212,10 +244,11 @@ where
|
||||||
fn verify_transaction_for_block(
|
fn verify_transaction_for_block(
|
||||||
tx_verification_data: &TransactionVerificationData,
|
tx_verification_data: &TransactionVerificationData,
|
||||||
current_chain_height: u64,
|
current_chain_height: u64,
|
||||||
|
time_for_time_lock: u64,
|
||||||
hf: HardFork,
|
hf: HardFork,
|
||||||
spent_kis: Arc<std::sync::Mutex<HashSet<[u8; 32]>>>,
|
spent_kis: Arc<std::sync::Mutex<HashSet<[u8; 32]>>>,
|
||||||
) -> Result<(), ConsensusError> {
|
) -> Result<(), ConsensusError> {
|
||||||
tracing::trace!(
|
tracing::debug!(
|
||||||
"Verifying transaction: {}",
|
"Verifying transaction: {}",
|
||||||
hex::encode(tx_verification_data.tx_hash)
|
hex::encode(tx_verification_data.tx_hash)
|
||||||
);
|
);
|
||||||
|
@ -228,7 +261,14 @@ 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.decoy_info, tx_version, &hf)?;
|
||||||
|
|
||||||
|
time_lock::check_all_time_locks(
|
||||||
|
&rings_member_info.time_locked_outs,
|
||||||
|
current_chain_height,
|
||||||
|
time_for_time_lock,
|
||||||
|
&hf,
|
||||||
|
)?;
|
||||||
|
|
||||||
let sum_outputs =
|
let sum_outputs =
|
||||||
outputs::check_outputs(&tx_verification_data.tx.prefix.outputs, &hf, tx_version)?;
|
outputs::check_outputs(&tx_verification_data.tx.prefix.outputs, &hf, tx_version)?;
|
||||||
|
@ -242,6 +282,15 @@ fn verify_transaction_for_block(
|
||||||
spent_kis,
|
spent_kis,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
if tx_version == &TxVersion::RingSignatures {
|
||||||
|
if sum_outputs >= sum_inputs {
|
||||||
|
return Err(ConsensusError::TransactionOutputsTooMuch);
|
||||||
|
}
|
||||||
|
// 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.
|
||||||
|
assert_eq!(tx_verification_data.fee, sum_inputs - sum_outputs);
|
||||||
|
}
|
||||||
|
|
||||||
sigs::verify_signatures(&tx_verification_data.tx, &rings_member_info.rings)?;
|
sigs::verify_signatures(&tx_verification_data.tx, &rings_member_info.rings)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use std::{
|
//! # Inputs
|
||||||
cmp::{max, min, Ordering},
|
//!
|
||||||
collections::HashSet,
|
//! This module contains all consensus rules for non-miner transaction inputs, excluding time locks.
|
||||||
sync::Arc,
|
//!
|
||||||
};
|
|
||||||
|
use std::{cmp::Ordering, collections::HashSet, sync::Arc};
|
||||||
|
|
||||||
use monero_serai::transaction::Input;
|
use monero_serai::transaction::Input;
|
||||||
use tower::{Service, ServiceExt};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
transactions::{
|
transactions::{
|
||||||
|
@ -69,6 +69,7 @@ pub(crate) fn check_key_images(
|
||||||
) -> Result<(), ConsensusError> {
|
) -> Result<(), ConsensusError> {
|
||||||
match input {
|
match input {
|
||||||
Input::ToKey { key_image, .. } => {
|
Input::ToKey { key_image, .. } => {
|
||||||
|
// this happens in monero-serai but we may as well duplicate the check.
|
||||||
if !key_image.is_torsion_free() {
|
if !key_image.is_torsion_free() {
|
||||||
return Err(ConsensusError::TransactionHasInvalidInput(
|
return Err(ConsensusError::TransactionHasInvalidInput(
|
||||||
"key image has torsion",
|
"key image has torsion",
|
||||||
|
@ -120,6 +121,7 @@ fn check_input_has_decoys(input: &Input) -> Result<(), ConsensusError> {
|
||||||
|
|
||||||
/// Checks that the ring members for the input are unique after hard-fork 6.
|
/// 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> {
|
fn check_ring_members_unique(input: &Input, hf: &HardFork) -> Result<(), ConsensusError> {
|
||||||
if hf >= &HardFork::V6 {
|
if hf >= &HardFork::V6 {
|
||||||
match input {
|
match input {
|
||||||
|
@ -139,6 +141,9 @@ fn check_ring_members_unique(input: &Input, hf: &HardFork) -> Result<(), Consens
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
fn check_inputs_sorted(inputs: &[Input], hf: &HardFork) -> Result<(), ConsensusError> {
|
||||||
let get_ki = |inp: &Input| match inp {
|
let get_ki = |inp: &Input| match inp {
|
||||||
Input::ToKey { key_image, .. } => key_image.compress().to_bytes(),
|
Input::ToKey { key_image, .. } => key_image.compress().to_bytes(),
|
||||||
|
@ -162,6 +167,9 @@ fn check_inputs_sorted(inputs: &[Input], hf: &HardFork) -> Result<(), ConsensusE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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(
|
fn check_10_block_lock(
|
||||||
ring_member_info: &TxRingMembersInfo,
|
ring_member_info: &TxRingMembersInfo,
|
||||||
current_chain_height: u64,
|
current_chain_height: u64,
|
||||||
|
@ -170,7 +178,7 @@ fn check_10_block_lock(
|
||||||
if hf >= &HardFork::V12 {
|
if hf >= &HardFork::V12 {
|
||||||
if ring_member_info.youngest_used_out_height + 10 > current_chain_height {
|
if ring_member_info.youngest_used_out_height + 10 > current_chain_height {
|
||||||
Err(ConsensusError::TransactionHasInvalidRing(
|
Err(ConsensusError::TransactionHasInvalidRing(
|
||||||
"tx has one ring member which is too younge",
|
"tx has one ring member which is too young",
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -203,6 +211,10 @@ fn sum_inputs_v1(inputs: &[Input]) -> Result<u64, ConsensusError> {
|
||||||
Ok(sum)
|
Ok(sum)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks all input consensus rules.
|
||||||
|
///
|
||||||
|
/// TODO: list rules.
|
||||||
|
///
|
||||||
pub fn check_inputs(
|
pub fn check_inputs(
|
||||||
inputs: &[Input],
|
inputs: &[Input],
|
||||||
ring_member_info: &TxRingMembersInfo,
|
ring_member_info: &TxRingMembersInfo,
|
||||||
|
@ -219,6 +231,8 @@ pub fn check_inputs(
|
||||||
|
|
||||||
if let Some(decoy_info) = &ring_member_info.decoy_info {
|
if let Some(decoy_info) = &ring_member_info.decoy_info {
|
||||||
check_decoy_info(decoy_info, hf)?;
|
check_decoy_info(decoy_info, hf)?;
|
||||||
|
} else {
|
||||||
|
assert_eq!(hf, &HardFork::V1);
|
||||||
}
|
}
|
||||||
|
|
||||||
for input in inputs {
|
for input in inputs {
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
//! # Outputs
|
||||||
|
//!
|
||||||
|
//! Consensus rules relating to non-miner transaction outputs
|
||||||
|
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use monero_serai::transaction::Output;
|
use monero_serai::transaction::Output;
|
||||||
|
@ -127,11 +131,11 @@ pub fn check_outputs(
|
||||||
hf: &HardFork,
|
hf: &HardFork,
|
||||||
tx_version: &TxVersion,
|
tx_version: &TxVersion,
|
||||||
) -> Result<u64, ConsensusError> {
|
) -> Result<u64, ConsensusError> {
|
||||||
check_output_types(outputs, &hf)?;
|
check_output_types(outputs, hf)?;
|
||||||
check_output_keys(outputs)?;
|
check_output_keys(outputs)?;
|
||||||
|
|
||||||
match tx_version {
|
match tx_version {
|
||||||
TxVersion::RingSignatures => sum_outputs_v1(outputs, &hf),
|
TxVersion::RingSignatures => sum_outputs_v1(outputs, hf),
|
||||||
_ => todo!("RingCT"),
|
_ => todo!("RingCT"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
//! ring members of inputs. This module does minimal consensus checks, only when needed, and should not be relied
|
//! ring members of inputs. This module does minimal consensus checks, only when needed, and should not be relied
|
||||||
//! upon to do any.
|
//! upon to do any.
|
||||||
//!
|
//!
|
||||||
|
//! The data collected by this module can be used to perform consensus checks.
|
||||||
|
//!
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
cmp::{max, min},
|
cmp::{max, min},
|
||||||
|
@ -13,8 +15,8 @@ use std::{
|
||||||
|
|
||||||
use curve25519_dalek::EdwardsPoint;
|
use curve25519_dalek::EdwardsPoint;
|
||||||
use monero_serai::{
|
use monero_serai::{
|
||||||
ringct::{mlsag::RingMatrix, RctType},
|
ringct::RctType,
|
||||||
transaction::{Input, Timelock, Transaction},
|
transaction::{Input, Timelock},
|
||||||
};
|
};
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
|
|
||||||
|
@ -23,168 +25,10 @@ use crate::{
|
||||||
DatabaseResponse, HardFork, OutputOnChain,
|
DatabaseResponse, HardFork, OutputOnChain,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Gets the absolute offsets from the relative offsets.
|
/// Fills the `rings_member_info` field on the inputted [`TransactionVerificationData`].
|
||||||
///
|
///
|
||||||
/// This function will return an error if the relative offsets are empty.
|
/// This function batch gets all the ring members for the inputted transactions and fills in data about
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#inputs-must-have-decoys
|
/// them, like the youngest used out and the time locks.
|
||||||
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 outputs 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
|
|
||||||
///
|
|
||||||
pub 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_insert_with(HashSet::new)
|
|
||||||
.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)]
|
|
||||||
pub enum Rings {
|
|
||||||
/// Legacy, pre-ringCT, ring.
|
|
||||||
Legacy(Vec<Vec<EdwardsPoint>>),
|
|
||||||
/// TODO:
|
|
||||||
RingCT,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Rings {
|
|
||||||
/// Builds the rings for the transaction inputs, from the given outputs.
|
|
||||||
pub fn new(outputs: Vec<Vec<&OutputOnChain>>, rct_type: RctType) -> Rings {
|
|
||||||
match rct_type {
|
|
||||||
RctType::Null => Rings::Legacy(
|
|
||||||
outputs
|
|
||||||
.into_iter()
|
|
||||||
.map(|inp_outs| inp_outs.into_iter().map(|out| out.key).collect())
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
_ => todo!("RingCT"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TxRingMembersInfo {
|
|
||||||
pub fn new(
|
|
||||||
used_outs: Vec<Vec<&OutputOnChain>>,
|
|
||||||
decoy_info: Option<DecoyInfo>,
|
|
||||||
rct_type: RctType,
|
|
||||||
) -> TxRingMembersInfo {
|
|
||||||
TxRingMembersInfo {
|
|
||||||
youngest_used_out_height: used_outs
|
|
||||||
.iter()
|
|
||||||
.map(|inp_outs| {
|
|
||||||
inp_outs
|
|
||||||
.iter()
|
|
||||||
.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, rct_type),
|
|
||||||
decoy_info,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the ring members for the inputs from the outputs on the chain.
|
|
||||||
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>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fills the `rings_member_info` field on the inputted [`TransactionVerificationData`]
|
|
||||||
pub async fn batch_fill_ring_member_info<D: Database + Clone + Send + Sync + 'static>(
|
pub async fn batch_fill_ring_member_info<D: Database + Clone + Send + Sync + 'static>(
|
||||||
txs_verification_data: &[Arc<TransactionVerificationData>],
|
txs_verification_data: &[Arc<TransactionVerificationData>],
|
||||||
hf: &HardFork,
|
hf: &HardFork,
|
||||||
|
@ -230,6 +74,173 @@ pub async fn batch_fill_ring_member_info<D: Database + Clone + Send + Sync + 'st
|
||||||
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_insert_with(HashSet::new)
|
||||||
|
.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)]
|
||||||
|
pub enum Rings {
|
||||||
|
/// Legacy, pre-ringCT, rings.
|
||||||
|
Legacy(Vec<Vec<EdwardsPoint>>),
|
||||||
|
/// TODO:
|
||||||
|
RingCT,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rings {
|
||||||
|
/// Builds the rings for the transaction inputs, from the given outputs.
|
||||||
|
fn new(outputs: Vec<Vec<&OutputOnChain>>, rct_type: RctType) -> Rings {
|
||||||
|
match rct_type {
|
||||||
|
RctType::Null => Rings::Legacy(
|
||||||
|
outputs
|
||||||
|
.into_iter()
|
||||||
|
.map(|inp_outs| inp_outs.into_iter().map(|out| out.key).collect())
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
_ => todo!("RingCT"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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>,
|
||||||
|
rct_type: RctType,
|
||||||
|
) -> 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, rct_type),
|
||||||
|
decoy_info,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
/// A struct holding information about the inputs and their decoys.
|
||||||
///
|
///
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html
|
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html
|
||||||
|
|
|
@ -1,18 +1,45 @@
|
||||||
use std::cmp::min;
|
//! # Time Locks
|
||||||
|
//!
|
||||||
|
//! This module contains the checks for time locks, using the `check_all_time_locks` function.
|
||||||
|
//!
|
||||||
use monero_serai::transaction::Timelock;
|
use monero_serai::transaction::Timelock;
|
||||||
|
|
||||||
use crate::{context::difficulty::DifficultyCache, helper::current_time, HardFork};
|
use crate::{ConsensusError, HardFork};
|
||||||
|
|
||||||
const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: u64 = 60;
|
/// 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.
|
/// Checks if an outputs unlock time has passed.
|
||||||
///
|
///
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#unlock-time
|
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#unlock-time
|
||||||
pub fn output_unlocked(
|
fn output_unlocked(
|
||||||
time_lock: &Timelock,
|
time_lock: &Timelock,
|
||||||
difficulty_cache: &DifficultyCache,
|
|
||||||
current_chain_height: u64,
|
current_chain_height: u64,
|
||||||
|
current_time_lock_timestamp: u64,
|
||||||
hf: &HardFork,
|
hf: &HardFork,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
match *time_lock {
|
match *time_lock {
|
||||||
|
@ -21,7 +48,7 @@ pub fn output_unlocked(
|
||||||
check_block_time_lock(unlock_height.try_into().unwrap(), current_chain_height)
|
check_block_time_lock(unlock_height.try_into().unwrap(), current_chain_height)
|
||||||
}
|
}
|
||||||
Timelock::Time(unlock_time) => {
|
Timelock::Time(unlock_time) => {
|
||||||
check_timestamp_time_lock(unlock_time, difficulty_cache, current_chain_height, hf)
|
check_timestamp_time_lock(unlock_time, current_time_lock_timestamp, hf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,39 +61,14 @@ fn check_block_time_lock(unlock_height: u64, current_chain_height: u64) -> bool
|
||||||
unlock_height >= current_chain_height
|
unlock_height >= current_chain_height
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the timestamp the should be used when checking locked outputs.
|
/// ///
|
||||||
///
|
|
||||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#getting-the-current-time
|
|
||||||
fn get_current_timestamp(
|
|
||||||
difficulty_cache: &DifficultyCache,
|
|
||||||
current_chain_height: u64,
|
|
||||||
hf: &HardFork,
|
|
||||||
) -> u64 {
|
|
||||||
if hf < &HardFork::V13 || current_chain_height < BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW {
|
|
||||||
current_time()
|
|
||||||
} else {
|
|
||||||
let median = difficulty_cache
|
|
||||||
.median_timestamp(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW.try_into().unwrap());
|
|
||||||
let adjusted_median =
|
|
||||||
median + (BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW + 1) * hf.block_time().as_secs() / 2;
|
|
||||||
|
|
||||||
// This is safe as we just check we don't have less than 60 blocks in the chain.
|
|
||||||
let adjusted_top_block =
|
|
||||||
difficulty_cache.top_block_timestamp().unwrap() + hf.block_time().as_secs();
|
|
||||||
|
|
||||||
min(adjusted_median, adjusted_top_block)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns if a locked output, which uses a block height, can be spend.
|
/// 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
|
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#timestamp
|
||||||
fn check_timestamp_time_lock(
|
fn check_timestamp_time_lock(
|
||||||
unlock_timestamp: u64,
|
unlock_timestamp: u64,
|
||||||
difficulty_cache: &DifficultyCache,
|
current_time_lock_timestamp: u64,
|
||||||
current_chain_height: u64,
|
|
||||||
hf: &HardFork,
|
hf: &HardFork,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let timestamp = get_current_timestamp(difficulty_cache, current_chain_height, hf);
|
current_time_lock_timestamp + hf.block_time().as_secs() >= unlock_timestamp
|
||||||
timestamp + hf.block_time().as_secs() >= unlock_timestamp
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,12 +57,12 @@ pub struct AltBlock {
|
||||||
// ---- TRANSACTIONS ----
|
// ---- TRANSACTIONS ----
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
/// [`TransactionPruned`] is, as its name suggest, the pruned part of a transaction, which is the Transaction Prefix and its RingCT signatures.
|
/// [`TransactionPruned`] is, as its name suggest, the pruned part of a transaction, which is the Transaction Prefix and its RingCT ring.
|
||||||
/// This struct is used in the [`crate::table::txsprefix`] table.
|
/// This struct is used in the [`crate::table::txsprefix`] table.
|
||||||
pub struct TransactionPruned {
|
pub struct TransactionPruned {
|
||||||
/// The transaction prefix.
|
/// The transaction prefix.
|
||||||
pub prefix: TransactionPrefix,
|
pub prefix: TransactionPrefix,
|
||||||
/// The RingCT signatures, will only contain the 'sig' field.
|
/// The RingCT ring, will only contain the 'sig' field.
|
||||||
pub rct_signatures: RctSig,
|
pub rct_signatures: RctSig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ impl bincode::Decode for TransactionPruned {
|
||||||
|
|
||||||
// Handle the prefix accordingly to its version
|
// Handle the prefix accordingly to its version
|
||||||
match *prefix.version {
|
match *prefix.version {
|
||||||
// First transaction format, Pre-RingCT, so the signatures are None
|
// First transaction format, Pre-RingCT, so the ring are None
|
||||||
1 => Ok(TransactionPruned {
|
1 => Ok(TransactionPruned {
|
||||||
prefix,
|
prefix,
|
||||||
rct_signatures: RctSig { sig: None, p: None },
|
rct_signatures: RctSig { sig: None, p: None },
|
||||||
|
@ -94,7 +94,7 @@ impl bincode::Decode for TransactionPruned {
|
||||||
rct_signatures,
|
rct_signatures,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Otherwise get the RingCT signatures for the tx inputs
|
// Otherwise get the RingCT ring for the tx inputs
|
||||||
if let Some(sig) = RctSigBase::consensus_decode(&mut r, inputs, outputs)
|
if let Some(sig) = RctSigBase::consensus_decode(&mut r, inputs, outputs)
|
||||||
.map_err(|_| bincode::error::DecodeError::Other("Monero-rs decoding failed"))?
|
.map_err(|_| bincode::error::DecodeError::Other("Monero-rs decoding failed"))?
|
||||||
{
|
{
|
||||||
|
@ -123,10 +123,10 @@ impl bincode::Encode for TransactionPruned {
|
||||||
let buf = monero::consensus::serialize(&self.prefix);
|
let buf = monero::consensus::serialize(&self.prefix);
|
||||||
writer.write(&buf)?;
|
writer.write(&buf)?;
|
||||||
match *self.prefix.version {
|
match *self.prefix.version {
|
||||||
1 => {} // First transaction format, Pre-RingCT, so the there is no Rct signatures to add
|
1 => {} // First transaction format, Pre-RingCT, so the there is no Rct ring to add
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(sig) = &self.rct_signatures.sig {
|
if let Some(sig) = &self.rct_signatures.sig {
|
||||||
// If there is signatures then we append it at the end
|
// If there is ring then we append it at the end
|
||||||
let buf = monero::consensus::serialize(sig);
|
let buf = monero::consensus::serialize(sig);
|
||||||
writer.write(&buf)?;
|
writer.write(&buf)?;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue