finish rules for v1 txs - clean up is needed

This commit is contained in:
Boog900 2023-10-24 20:17:16 +01:00
parent 9b7f778f60
commit b727062e97
No known key found for this signature in database
GPG key ID: 5401367FB7302004
12 changed files with 365 additions and 291 deletions

View file

@ -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;

View file

@ -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?

View file

@ -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(&current_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,

View file

@ -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<(

View file

@ -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,

View file

@ -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 {

View file

@ -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(())

View file

@ -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 {

View file

@ -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"),
}
}

View file

@ -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

View file

@ -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
}

View file

@ -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)?;
}