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