mirror of
https://github.com/Cuprate/cuprate.git
synced 2024-12-22 11:39:26 +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")]
|
||||
|
||||
use futures::Sink;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io::Read;
|
||||
use std::ops::Range;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use rayon::prelude::*;
|
||||
use tower::{Service, ServiceExt};
|
||||
use tracing::instrument;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
|
||||
use cuprate_common::Network;
|
||||
|
@ -53,7 +47,8 @@ where
|
|||
{
|
||||
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);
|
||||
|
||||
|
@ -79,10 +74,6 @@ where
|
|||
let mut current_height = start_height;
|
||||
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 {
|
||||
let next_batch_size = rpc_config.read().unwrap().block_batch_size();
|
||||
|
||||
|
@ -96,56 +87,10 @@ where
|
|||
)),
|
||||
);
|
||||
|
||||
let (DatabaseResponse::BlockBatchInRange(blocks), time_to_retrieve_batch) =
|
||||
current_fut.await??
|
||||
else {
|
||||
let (DatabaseResponse::BlockBatchInRange(blocks), _) = current_fut.await?? else {
|
||||
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!(
|
||||
"Handling batch: {:?}, chain height: {}",
|
||||
current_height..(current_height + blocks.len() as u64),
|
||||
|
@ -190,8 +135,6 @@ where
|
|||
cache.write().unwrap().save(&save_file)?;
|
||||
}
|
||||
}
|
||||
|
||||
time_to_verify_last_batch = time_to_verify_batch.elapsed().as_millis();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -200,7 +143,7 @@ where
|
|||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(LevelFilter::DEBUG)
|
||||
.with_max_level(LevelFilter::INFO)
|
||||
.init();
|
||||
|
||||
let network = Network::Mainnet;
|
||||
|
|
|
@ -132,6 +132,7 @@ where
|
|||
.oneshot(VerifyTxRequest::BatchSetupVerifyBlock {
|
||||
txs,
|
||||
current_chain_height: context.chain_height,
|
||||
time_for_time_lock: context.current_adjusted_timestamp_for_time_lock(),
|
||||
hf: context.current_hard_fork,
|
||||
})
|
||||
.await?
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
//!
|
||||
|
||||
use std::{
|
||||
cmp::min,
|
||||
future::Future,
|
||||
ops::{Deref, DerefMut},
|
||||
pin::Pin,
|
||||
|
@ -17,7 +18,7 @@ use futures::FutureExt;
|
|||
use tokio::sync::RwLock;
|
||||
use tower::{Service, ServiceExt};
|
||||
|
||||
use crate::{ConsensusError, Database, DatabaseRequest, DatabaseResponse};
|
||||
use crate::{helper::current_time, ConsensusError, Database, DatabaseRequest, DatabaseResponse};
|
||||
|
||||
pub mod difficulty;
|
||||
mod hardforks;
|
||||
|
@ -27,7 +28,7 @@ pub use difficulty::DifficultyCacheConfig;
|
|||
pub use hardforks::{HardFork, HardForkConfig};
|
||||
pub use weight::BlockWeightsCacheConfig;
|
||||
|
||||
const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: usize = 60;
|
||||
const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: u64 = 60;
|
||||
|
||||
pub struct ContextConfig {
|
||||
hard_fork_cfg: HardForkConfig,
|
||||
|
@ -143,11 +144,10 @@ pub struct BlockChainContext {
|
|||
pub median_weight_for_block_reward: usize,
|
||||
/// The amount of coins minted already.
|
||||
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
|
||||
/// [`BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW`] blocks.
|
||||
pub median_block_timestamp: Option<u64>,
|
||||
top_block_timestamp: Option<u64>,
|
||||
/// The height of the chain.
|
||||
pub chain_height: u64,
|
||||
/// The top blocks hash
|
||||
|
@ -157,6 +157,29 @@ pub struct 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 {
|
||||
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_weight_for_block_reward: weight_cache.median_for_block_reward(¤t_hf),
|
||||
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_timestamp(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW),
|
||||
.median_timestamp(usize::try_from(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW).unwrap()),
|
||||
chain_height: *chain_height,
|
||||
top_hash: *top_block_hash,
|
||||
current_hard_fork: current_hf,
|
||||
|
|
|
@ -31,18 +31,22 @@ where
|
|||
D: Database + Clone + Send + Sync + '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 block_svc = block::BlockVerifierService::new(context_svc.clone(), tx_svc.clone());
|
||||
Ok((block_svc, tx_svc, context_svc_updater))
|
||||
}
|
||||
|
||||
// TODO: split this enum up.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ConsensusError {
|
||||
#[error("Miner transaction invalid: {0}")]
|
||||
MinerTransaction(&'static str),
|
||||
#[error("Transaction sig invalid: {0}")]
|
||||
TransactionSignatureInvalid(&'static str),
|
||||
#[error("Transaction has too high output amount")]
|
||||
TransactionOutputsTooMuch,
|
||||
#[error("Transaction inputs overflow")]
|
||||
TransactionInputsOverflow,
|
||||
#[error("Transaction outputs overflow")]
|
||||
|
@ -111,6 +115,8 @@ pub enum DatabaseRequest {
|
|||
|
||||
Outputs(HashMap<u64, HashSet<u64>>),
|
||||
NumberOutputsWithAmount(u64),
|
||||
|
||||
CheckKIsNotSpent(HashSet<[u8; 32]>),
|
||||
|
||||
#[cfg(feature = "binaries")]
|
||||
BlockBatchInRange(std::ops::Range<u64>),
|
||||
|
@ -129,6 +135,8 @@ pub enum DatabaseResponse {
|
|||
Outputs(HashMap<u64, HashMap<u64, OutputOnChain>>),
|
||||
NumberOutputsWithAmount(usize),
|
||||
|
||||
CheckKIsNotSpent(bool),
|
||||
|
||||
#[cfg(feature = "binaries")]
|
||||
BlockBatchInRange(
|
||||
Vec<(
|
||||
|
|
|
@ -295,7 +295,13 @@ impl<R: RpcConnection + Send + Sync + 'static> tower::Service<DatabaseRequest> f
|
|||
}
|
||||
.instrument(span)
|
||||
.boxed(),
|
||||
|
||||
DatabaseRequest::CheckKIsNotSpent(kis) => async move {
|
||||
Ok(DatabaseResponse::CheckKIsNotSpent(
|
||||
cache.read().unwrap().are_kis_spent(kis),
|
||||
))
|
||||
}
|
||||
.instrument(span)
|
||||
.boxed(),
|
||||
DatabaseRequest::GeneratedCoins => async move {
|
||||
Ok(DatabaseResponse::GeneratedCoins(
|
||||
cache.read().unwrap().already_generated_coins,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use std::collections::HashSet;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::{
|
||||
|
@ -7,7 +8,7 @@ use std::{
|
|||
};
|
||||
|
||||
use bincode::{Decode, Encode};
|
||||
use monero_serai::transaction::{Timelock, Transaction};
|
||||
use monero_serai::transaction::{Input, Timelock, Transaction};
|
||||
use tracing_subscriber::fmt::MakeWriter;
|
||||
|
||||
use cuprate_common::Network;
|
||||
|
@ -24,6 +25,7 @@ pub struct ScanningCache {
|
|||
// network: u8,
|
||||
numb_outs: HashMap<u64, u64>,
|
||||
time_locked_out: HashMap<[u8; 32], u64>,
|
||||
kis: HashSet<[u8; 32]>,
|
||||
pub already_generated_coins: u64,
|
||||
/// The height of the *next* block to scan.
|
||||
pub height: u64,
|
||||
|
@ -67,12 +69,23 @@ impl ScanningCache {
|
|||
.outputs
|
||||
.iter()
|
||||
.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.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 {
|
||||
let time_lock = self.time_locked_out.get(tx).copied().unwrap_or(0);
|
||||
match time_lock {
|
||||
|
|
|
@ -19,7 +19,7 @@ mod inputs;
|
|||
pub(crate) mod outputs;
|
||||
mod ring;
|
||||
mod sigs;
|
||||
//mod time_lock;
|
||||
mod time_lock;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum TxVersion {
|
||||
|
@ -71,6 +71,7 @@ pub enum VerifyTxRequest {
|
|||
Block {
|
||||
txs: Vec<Arc<TransactionVerificationData>>,
|
||||
current_chain_height: u64,
|
||||
time_for_time_lock: u64,
|
||||
hf: HardFork,
|
||||
},
|
||||
/// Batches the setup of [`TransactionVerificationData`] and verifies the transactions
|
||||
|
@ -78,6 +79,7 @@ pub enum VerifyTxRequest {
|
|||
BatchSetupVerifyBlock {
|
||||
txs: Vec<Transaction>,
|
||||
current_chain_height: u64,
|
||||
time_for_time_lock: u64,
|
||||
hf: HardFork,
|
||||
},
|
||||
}
|
||||
|
@ -123,14 +125,29 @@ where
|
|||
VerifyTxRequest::Block {
|
||||
txs,
|
||||
current_chain_height,
|
||||
time_for_time_lock,
|
||||
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 {
|
||||
txs,
|
||||
current_chain_height,
|
||||
time_for_time_lock,
|
||||
hf,
|
||||
} => batch_setup_verify_transactions_for_block(database, txs, current_chain_height, hf)
|
||||
.boxed(),
|
||||
} => batch_setup_verify_transactions_for_block(
|
||||
database,
|
||||
txs,
|
||||
current_chain_height,
|
||||
time_for_time_lock,
|
||||
hf,
|
||||
)
|
||||
.boxed(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -166,6 +183,7 @@ async fn batch_setup_verify_transactions_for_block<D>(
|
|||
database: D,
|
||||
txs: Vec<Transaction>,
|
||||
current_chain_height: u64,
|
||||
time_for_time_lock: u64,
|
||||
hf: HardFork,
|
||||
) -> Result<VerifyTxResponse, ConsensusError>
|
||||
where
|
||||
|
@ -180,7 +198,14 @@ where
|
|||
.await
|
||||
.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))
|
||||
}
|
||||
|
||||
|
@ -189,6 +214,7 @@ async fn verify_transactions_for_block<D>(
|
|||
database: D,
|
||||
txs: Vec<Arc<TransactionVerificationData>>,
|
||||
current_chain_height: u64,
|
||||
time_for_time_lock: u64,
|
||||
hf: HardFork,
|
||||
) -> Result<VerifyTxResponse, ConsensusError>
|
||||
where
|
||||
|
@ -202,7 +228,13 @@ where
|
|||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
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(
|
||||
tx_verification_data: &TransactionVerificationData,
|
||||
current_chain_height: u64,
|
||||
time_for_time_lock: u64,
|
||||
hf: HardFork,
|
||||
spent_kis: Arc<std::sync::Mutex<HashSet<[u8; 32]>>>,
|
||||
) -> Result<(), ConsensusError> {
|
||||
tracing::trace!(
|
||||
tracing::debug!(
|
||||
"Verifying transaction: {}",
|
||||
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!"),
|
||||
};
|
||||
|
||||
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 =
|
||||
outputs::check_outputs(&tx_verification_data.tx.prefix.outputs, &hf, tx_version)?;
|
||||
|
@ -242,6 +282,15 @@ fn verify_transaction_for_block(
|
|||
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)?;
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
use std::{
|
||||
cmp::{max, min, Ordering},
|
||||
collections::HashSet,
|
||||
sync::Arc,
|
||||
};
|
||||
//! # Inputs
|
||||
//!
|
||||
//! This module contains all consensus rules for non-miner transaction inputs, excluding time locks.
|
||||
//!
|
||||
|
||||
use std::{cmp::Ordering, collections::HashSet, sync::Arc};
|
||||
|
||||
use monero_serai::transaction::Input;
|
||||
use tower::{Service, ServiceExt};
|
||||
|
||||
use crate::{
|
||||
transactions::{
|
||||
|
@ -69,6 +69,7 @@ pub(crate) fn check_key_images(
|
|||
) -> Result<(), ConsensusError> {
|
||||
match input {
|
||||
Input::ToKey { key_image, .. } => {
|
||||
// this happens in monero-serai but we may as well duplicate the check.
|
||||
if !key_image.is_torsion_free() {
|
||||
return Err(ConsensusError::TransactionHasInvalidInput(
|
||||
"key image has torsion",
|
||||
|
@ -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.
|
||||
///
|
||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#unique-ring-members
|
||||
fn check_ring_members_unique(input: &Input, hf: &HardFork) -> Result<(), ConsensusError> {
|
||||
if hf >= &HardFork::V6 {
|
||||
match input {
|
||||
|
@ -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> {
|
||||
let get_ki = |inp: &Input| match inp {
|
||||
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(
|
||||
ring_member_info: &TxRingMembersInfo,
|
||||
current_chain_height: u64,
|
||||
|
@ -170,7 +178,7 @@ fn check_10_block_lock(
|
|||
if hf >= &HardFork::V12 {
|
||||
if ring_member_info.youngest_used_out_height + 10 > current_chain_height {
|
||||
Err(ConsensusError::TransactionHasInvalidRing(
|
||||
"tx has one ring member which is too younge",
|
||||
"tx has one ring member which is too young",
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
|
@ -203,6 +211,10 @@ fn sum_inputs_v1(inputs: &[Input]) -> Result<u64, ConsensusError> {
|
|||
Ok(sum)
|
||||
}
|
||||
|
||||
/// Checks all input consensus rules.
|
||||
///
|
||||
/// TODO: list rules.
|
||||
///
|
||||
pub fn check_inputs(
|
||||
inputs: &[Input],
|
||||
ring_member_info: &TxRingMembersInfo,
|
||||
|
@ -219,6 +231,8 @@ pub fn check_inputs(
|
|||
|
||||
if let Some(decoy_info) = &ring_member_info.decoy_info {
|
||||
check_decoy_info(decoy_info, hf)?;
|
||||
} else {
|
||||
assert_eq!(hf, &HardFork::V1);
|
||||
}
|
||||
|
||||
for input in inputs {
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
//! # Outputs
|
||||
//!
|
||||
//! Consensus rules relating to non-miner transaction outputs
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use monero_serai::transaction::Output;
|
||||
|
@ -127,11 +131,11 @@ pub fn check_outputs(
|
|||
hf: &HardFork,
|
||||
tx_version: &TxVersion,
|
||||
) -> Result<u64, ConsensusError> {
|
||||
check_output_types(outputs, &hf)?;
|
||||
check_output_types(outputs, hf)?;
|
||||
check_output_keys(outputs)?;
|
||||
|
||||
match tx_version {
|
||||
TxVersion::RingSignatures => sum_outputs_v1(outputs, &hf),
|
||||
TxVersion::RingSignatures => sum_outputs_v1(outputs, hf),
|
||||
_ => todo!("RingCT"),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
//! ring members of inputs. This module does minimal consensus checks, only when needed, and should not be relied
|
||||
//! upon to do any.
|
||||
//!
|
||||
//! The data collected by this module can be used to perform consensus checks.
|
||||
//!
|
||||
|
||||
use std::{
|
||||
cmp::{max, min},
|
||||
|
@ -13,8 +15,8 @@ use std::{
|
|||
|
||||
use curve25519_dalek::EdwardsPoint;
|
||||
use monero_serai::{
|
||||
ringct::{mlsag::RingMatrix, RctType},
|
||||
transaction::{Input, Timelock, Transaction},
|
||||
ringct::RctType,
|
||||
transaction::{Input, Timelock},
|
||||
};
|
||||
use tower::ServiceExt;
|
||||
|
||||
|
@ -23,168 +25,10 @@ use crate::{
|
|||
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.
|
||||
/// 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 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`]
|
||||
/// This function batch gets all the ring members for the inputted transactions and fills in data about
|
||||
/// them, like the youngest used out and the time locks.
|
||||
pub async fn batch_fill_ring_member_info<D: Database + Clone + Send + Sync + 'static>(
|
||||
txs_verification_data: &[Arc<TransactionVerificationData>],
|
||||
hf: &HardFork,
|
||||
|
@ -230,6 +74,173 @@ pub async fn batch_fill_ring_member_info<D: Database + Clone + Send + Sync + 'st
|
|||
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.
|
||||
///
|
||||
/// 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 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.
|
||||
///
|
||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#unlock-time
|
||||
pub fn output_unlocked(
|
||||
fn output_unlocked(
|
||||
time_lock: &Timelock,
|
||||
difficulty_cache: &DifficultyCache,
|
||||
current_chain_height: u64,
|
||||
current_time_lock_timestamp: u64,
|
||||
hf: &HardFork,
|
||||
) -> bool {
|
||||
match *time_lock {
|
||||
|
@ -21,7 +48,7 @@ pub fn output_unlocked(
|
|||
check_block_time_lock(unlock_height.try_into().unwrap(), current_chain_height)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#timestamp
|
||||
fn check_timestamp_time_lock(
|
||||
unlock_timestamp: u64,
|
||||
difficulty_cache: &DifficultyCache,
|
||||
current_chain_height: u64,
|
||||
current_time_lock_timestamp: u64,
|
||||
hf: &HardFork,
|
||||
) -> bool {
|
||||
let timestamp = get_current_timestamp(difficulty_cache, current_chain_height, hf);
|
||||
timestamp + hf.block_time().as_secs() >= unlock_timestamp
|
||||
current_time_lock_timestamp + hf.block_time().as_secs() >= unlock_timestamp
|
||||
}
|
||||
|
|
|
@ -57,12 +57,12 @@ pub struct AltBlock {
|
|||
// ---- TRANSACTIONS ----
|
||||
|
||||
#[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.
|
||||
pub struct TransactionPruned {
|
||||
/// The transaction prefix.
|
||||
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,
|
||||
}
|
||||
|
||||
|
@ -80,7 +80,7 @@ impl bincode::Decode for TransactionPruned {
|
|||
|
||||
// Handle the prefix accordingly to its 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 {
|
||||
prefix,
|
||||
rct_signatures: RctSig { sig: None, p: None },
|
||||
|
@ -94,7 +94,7 @@ impl bincode::Decode for TransactionPruned {
|
|||
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)
|
||||
.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);
|
||||
writer.write(&buf)?;
|
||||
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 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);
|
||||
writer.write(&buf)?;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue