Skip to main content

cuprate_consensus/transactions/
contextual_data.rs

1//! # Contextual Data
2//!
3//! This module fills [`TxRingMembersInfo`] which is a struct made up from blockchain information about the
4//! ring members of inputs. This module does minimal consensus checks, only when needed, and should not be relied
5//! upon to do any.
6//!
7//! The data collected by this module can be used to perform consensus checks.
8//!
9//! ## Why not use the context service?
10//!
11//! Because this data is unique for *every* transaction and the context service is just for blockchain state data.
12//!
13
14use std::{borrow::Cow, collections::HashSet};
15
16use indexmap::IndexMap;
17use monero_oxide::transaction::{Input, Timelock};
18use tower::ServiceExt;
19use tracing::instrument;
20
21use cuprate_consensus_rules::{
22    transactions::{
23        get_absolute_offsets, insert_ring_member_ids, DecoyInfo, Rings, TransactionError,
24        TxRingMembersInfo,
25    },
26    ConsensusError, HardFork, TxVersion,
27};
28
29use cuprate_types::{
30    blockchain::{BlockchainReadRequest, BlockchainResponse},
31    output_cache::OutputCache,
32    OutputOnChain,
33};
34
35use crate::{
36    transactions::TransactionVerificationData, Database, ExtendedConsensusError,
37    VerificationContext,
38};
39
40/// Get the ring members for the inputs from the outputs on the chain.
41///
42/// Will error if `outputs` does not contain the outputs needed.
43fn get_ring_members_for_inputs(
44    get_outputs: impl Fn(u64, u64) -> Option<OutputOnChain>,
45    inputs: &[Input],
46) -> Result<Vec<Vec<OutputOnChain>>, TransactionError> {
47    inputs
48        .iter()
49        .map(|inp| match inp {
50            Input::ToKey {
51                amount,
52                key_offsets,
53                ..
54            } => {
55                let offsets = get_absolute_offsets(key_offsets)?;
56                Ok(offsets
57                    .iter()
58                    .map(|offset| {
59                        get_outputs(amount.unwrap_or(0), *offset)
60                            .ok_or(TransactionError::RingMemberNotFoundOrInvalid)
61                    })
62                    .collect::<Result<_, TransactionError>>()?)
63            }
64            Input::Gen(_) => Err(TransactionError::IncorrectInputType),
65        })
66        .collect::<Result<_, TransactionError>>()
67}
68
69/// Construct a [`TxRingMembersInfo`] struct.
70///
71/// The used outs must be all the ring members used in the transaction's inputs.
72pub fn new_ring_member_info(
73    used_outs: Vec<Vec<OutputOnChain>>,
74    decoy_info: Option<DecoyInfo>,
75    tx_version: TxVersion,
76) -> Result<TxRingMembersInfo, TransactionError> {
77    Ok(TxRingMembersInfo {
78        youngest_used_out_height: used_outs
79            .iter()
80            .map(|inp_outs| {
81                inp_outs
82                    // Use the last output declared in the ring as the youngest.
83                    .last()
84                    .map(|out| out.height)
85                    .expect("Input must have ring members")
86            })
87            // the output with the highest height is the youngest
88            .max()
89            .expect("Tx must have inputs"),
90        time_locked_outs: used_outs
91            .iter()
92            .flat_map(|inp_outs| {
93                inp_outs
94                    .iter()
95                    .filter_map(|out| match out.time_lock {
96                        Timelock::None => None,
97                        Timelock::Block(_) | Timelock::Time(_) => Some(out.time_lock),
98                    })
99                    .collect::<Vec<_>>()
100            })
101            .collect(),
102        rings: new_rings(used_outs, tx_version),
103        decoy_info,
104    })
105}
106
107/// Builds the [`Rings`] for the transaction inputs, from the given outputs.
108fn new_rings(outputs: Vec<Vec<OutputOnChain>>, tx_version: TxVersion) -> Rings {
109    match tx_version {
110        TxVersion::RingSignatures => Rings::Legacy(
111            outputs
112                .into_iter()
113                .map(|inp_outs| inp_outs.into_iter().map(|out| out.key).collect::<Vec<_>>())
114                .collect::<Vec<_>>(),
115        ),
116        TxVersion::RingCT => Rings::RingCT(
117            outputs
118                .into_iter()
119                .map(|inp_outs| {
120                    inp_outs
121                        .into_iter()
122                        .map(|out| [out.key, out.commitment])
123                        .collect::<_>()
124                })
125                .collect::<_>(),
126        ),
127    }
128}
129
130/// Retrieves an [`OutputCache`] for the list of transactions.
131///
132/// The [`OutputCache`] will only contain the outputs currently in the blockchain.
133pub async fn get_output_cache<D: Database>(
134    txs_verification_data: impl Iterator<Item = &TransactionVerificationData>,
135    mut database: D,
136) -> Result<OutputCache, ExtendedConsensusError> {
137    let mut outputs = IndexMap::new();
138
139    for tx_v_data in txs_verification_data {
140        insert_ring_member_ids(&tx_v_data.tx.prefix().inputs, &mut outputs)
141            .map_err(ConsensusError::Transaction)?;
142    }
143
144    let BlockchainResponse::Outputs(outputs) = database
145        .ready()
146        .await?
147        .call(BlockchainReadRequest::Outputs {
148            outputs,
149            get_txid: false,
150        })
151        .await?
152    else {
153        unreachable!();
154    };
155
156    Ok(outputs)
157}
158
159/// Retrieves the [`TxRingMembersInfo`] for the inputted [`TransactionVerificationData`].
160///
161/// This function batch gets all the ring members for the inputted transactions and fills in data about
162/// them.
163pub async fn batch_get_ring_member_info<D: Database>(
164    txs_verification_data: impl Iterator<Item = &TransactionVerificationData> + Clone,
165    hf: HardFork,
166    verification_context: &mut VerificationContext<D>,
167) -> Result<Vec<TxRingMembersInfo>, ExtendedConsensusError> {
168    let outputs = match verification_context {
169        VerificationContext::BatchPrepareCache(cache) => Cow::Borrowed(&cache.output_cache),
170        VerificationContext::Database(database) => {
171            let mut outputs = IndexMap::new();
172
173            for tx_v_data in txs_verification_data.clone() {
174                insert_ring_member_ids(&tx_v_data.tx.prefix().inputs, &mut outputs)
175                    .map_err(ConsensusError::Transaction)?;
176            }
177
178            let BlockchainResponse::Outputs(outputs) = database
179                .ready()
180                .await?
181                .call(BlockchainReadRequest::Outputs {
182                    outputs,
183                    get_txid: false,
184                })
185                .await?
186            else {
187                unreachable!();
188            };
189
190            Cow::Owned(outputs)
191        }
192    };
193
194    Ok(txs_verification_data
195        .map(move |tx_v_data| {
196            let numb_outputs = |amt| outputs.number_outs_with_amount(amt);
197
198            let ring_members_for_tx = get_ring_members_for_inputs(
199                |amt, idx| outputs.get_output(amt, idx).copied(),
200                &tx_v_data.tx.prefix().inputs,
201            )
202            .map_err(ConsensusError::Transaction)?;
203
204            let decoy_info = if hf == HardFork::V1 {
205                None
206            } else {
207                // this data is only needed after hard-fork 1.
208                Some(
209                    DecoyInfo::new(&tx_v_data.tx.prefix().inputs, numb_outputs, hf)
210                        .map_err(ConsensusError::Transaction)?,
211                )
212            };
213
214            new_ring_member_info(ring_members_for_tx, decoy_info, tx_v_data.version)
215                .map_err(ConsensusError::Transaction)
216        })
217        .collect::<Result<_, _>>()?)
218}
219
220/// Refreshes the transactions' [`TxRingMembersInfo`], if needed.
221///
222/// # Panics
223/// This function panics if `hf == HardFork::V1` as decoy info
224/// should not be needed for V1.
225#[instrument(level = "debug", skip_all)]
226pub async fn batch_get_decoy_info<'a, 'b, D: Database>(
227    txs_verification_data: impl Iterator<Item = &'a TransactionVerificationData> + Clone,
228    hf: HardFork,
229    verification_context: &mut VerificationContext<D>,
230) -> Result<
231    impl Iterator<Item = Result<DecoyInfo, ConsensusError>> + sealed::Captures<(&'a (), &'b ())>,
232    ExtendedConsensusError,
233> {
234    // decoy info is not needed for V1.
235    assert_ne!(hf, HardFork::V1);
236
237    // Get all the different input amounts.
238    let unique_input_amounts = txs_verification_data
239        .clone()
240        .flat_map(|tx_info| {
241            tx_info.tx.prefix().inputs.iter().map(|input| match input {
242                Input::ToKey { amount, .. } => amount.unwrap_or(0),
243                Input::Gen(_) => 0,
244            })
245        })
246        .collect::<HashSet<_>>();
247
248    tracing::debug!(
249        "Getting the amount of outputs with certain amounts for {} amounts",
250        unique_input_amounts.len()
251    );
252
253    let outputs_with_amount = match verification_context {
254        VerificationContext::BatchPrepareCache(cache) => unique_input_amounts
255            .into_iter()
256            .map(|amount| (amount, cache.output_cache.number_outs_with_amount(amount)))
257            .collect(),
258        VerificationContext::Database(database) => {
259            let BlockchainResponse::NumberOutputsWithAmount(outputs_with_amount) = database
260                .ready()
261                .await?
262                .call(BlockchainReadRequest::NumberOutputsWithAmount(
263                    unique_input_amounts.into_iter().collect(),
264                ))
265                .await?
266            else {
267                unreachable!();
268            };
269
270            outputs_with_amount
271        }
272    };
273
274    Ok(txs_verification_data.map(move |tx_v_data| {
275        DecoyInfo::new(
276            &tx_v_data.tx.prefix().inputs,
277            |amt| outputs_with_amount.get(&amt).copied().unwrap_or(0),
278            hf,
279        )
280        .map_err(ConsensusError::Transaction)
281    }))
282}
283
284mod sealed {
285    /// TODO: Remove me when 2024 Rust
286    ///
287    /// <https://rust-lang.github.io/rfcs/3498-lifetime-capture-rules-2024.html#the-captures-trick>
288    pub trait Captures<U> {}
289    impl<T: ?Sized, U> Captures<U> for T {}
290}