Skip to main content

chia_sdk_test/full_node_simulator/
push_tx.rs

1use chia_consensus::{conditions::ELIGIBLE_FOR_DEDUP, validation_error::ErrorCode};
2use chia_protocol::{Bytes32, SpendBundle};
3use chia_sdk_coinset::PushTxResponse;
4use indexmap::IndexSet;
5
6use crate::SimulatorError;
7
8use super::{
9    FullNodeSimulator, FullNodeSimulatorPushTxResponse, ValidatedBundle, ValidatedSpend,
10    fast_forward::FastForwardResult,
11};
12
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14struct NormalizationState {
15    ordered_coin_ids: Vec<Bytes32>,
16    spend_bundle_id: Bytes32,
17}
18
19impl NormalizationState {
20    fn new(spend_bundle: &SpendBundle) -> Self {
21        Self {
22            ordered_coin_ids: spend_bundle
23                .coin_spends
24                .iter()
25                .map(|coin_spend| coin_spend.coin.coin_id())
26                .collect(),
27            spend_bundle_id: spend_bundle.name(),
28        }
29    }
30}
31
32#[derive(Debug, Default)]
33struct NormalizationProgress {
34    seen: IndexSet<NormalizationState>,
35}
36
37impl NormalizationProgress {
38    fn record(&mut self, spend_bundle: &SpendBundle) -> bool {
39        self.seen.insert(NormalizationState::new(spend_bundle))
40    }
41}
42
43impl FullNodeSimulator {
44    fn insert_mempool_item(
45        &mut self,
46        tx_id: Bytes32,
47        validated: ValidatedBundle,
48    ) -> Result<(), SimulatorError> {
49        let conflicting_tx_ids = self.conflicting_mempool_tx_ids(&validated);
50        if !conflicting_tx_ids.is_empty()
51            && !self.is_mempool_replacement(&validated, &conflicting_tx_ids)
52        {
53            return Err(SimulatorError::Validation(ErrorCode::MempoolConflict));
54        }
55        if !conflicting_tx_ids.is_empty() {
56            for tx_id in conflicting_tx_ids {
57                self.mempool.swap_remove(&tx_id);
58            }
59        }
60
61        self.mempool.insert(tx_id, validated);
62        Ok(())
63    }
64
65    fn mempool_rejects(&self, validated: &ValidatedBundle) -> bool {
66        let conflicting_tx_ids = self.conflicting_mempool_tx_ids(validated);
67        !conflicting_tx_ids.is_empty()
68            && !self.is_mempool_replacement(validated, &conflicting_tx_ids)
69    }
70
71    fn is_mempool_replacement(
72        &self,
73        validated: &ValidatedBundle,
74        conflicting_tx_ids: &[Bytes32],
75    ) -> bool {
76        let conflicting_removals = conflicting_tx_ids
77            .iter()
78            .filter_map(|tx_id| self.mempool.get(tx_id))
79            .flat_map(|item| item.removals.iter().copied())
80            .collect::<IndexSet<_>>();
81        let conflicting_fees = conflicting_tx_ids
82            .iter()
83            .filter_map(|tx_id| self.mempool.get(tx_id))
84            .map(|item| item.fee)
85            .sum::<u64>();
86
87        conflicting_removals
88            .iter()
89            .all(|coin_id| validated.removals.contains(coin_id))
90            && validated.fee > conflicting_fees
91    }
92
93    fn conflicting_mempool_tx_ids(&self, validated: &ValidatedBundle) -> Vec<Bytes32> {
94        self.mempool
95            .iter()
96            .filter(|(_, item)| Self::has_non_dedup_overlap(validated, item))
97            .map(|(tx_id, _)| *tx_id)
98            .collect()
99    }
100
101    fn has_non_dedup_overlap(lhs: &ValidatedBundle, rhs: &ValidatedBundle) -> bool {
102        lhs.removals.iter().any(|coin_id| {
103            rhs.removals.contains(coin_id) && !Self::removal_is_dedup_compatible(lhs, rhs, *coin_id)
104        })
105    }
106
107    fn removal_is_dedup_compatible(
108        lhs: &ValidatedBundle,
109        rhs: &ValidatedBundle,
110        coin_id: Bytes32,
111    ) -> bool {
112        let Some(lhs_spend) = lhs.spends.get(&coin_id) else {
113            return false;
114        };
115        let Some(rhs_spend) = rhs.spends.get(&coin_id) else {
116            return false;
117        };
118        Self::spends_are_dedup_compatible(lhs_spend, rhs_spend)
119    }
120
121    pub(super) fn spends_are_dedup_compatible(lhs: &ValidatedSpend, rhs: &ValidatedSpend) -> bool {
122        (lhs.flags & ELIGIBLE_FOR_DEDUP) != 0
123            && (rhs.flags & ELIGIBLE_FOR_DEDUP) != 0
124            && lhs.fingerprint.is_some()
125            && lhs.fingerprint == rhs.fingerprint
126    }
127
128    fn push_tx_success() -> FullNodeSimulatorPushTxResponse {
129        FullNodeSimulatorPushTxResponse {
130            response: PushTxResponse {
131                status: Some("SUCCESS".to_string()),
132                error: None,
133                success: true,
134            },
135            error: None,
136        }
137    }
138
139    fn push_tx_failure(error: SimulatorError) -> FullNodeSimulatorPushTxResponse {
140        FullNodeSimulatorPushTxResponse {
141            response: PushTxResponse {
142                status: Some("FAILED".to_string()),
143                error: Some(error.to_string()),
144                success: false,
145            },
146            error: Some(error),
147        }
148    }
149
150    pub fn push_tx(&mut self, spend_bundle: SpendBundle) -> PushTxResponse {
151        self.push_tx_detailed(spend_bundle).response
152    }
153
154    pub fn push_tx_detailed(
155        &mut self,
156        spend_bundle: SpendBundle,
157    ) -> FullNodeSimulatorPushTxResponse {
158        match self.normalize_and_insert(spend_bundle) {
159            Ok(()) => Self::push_tx_success(),
160            Err(error) => Self::push_tx_failure(error),
161        }
162    }
163
164    pub(super) fn normalize_and_insert(
165        &mut self,
166        mut spend_bundle: SpendBundle,
167    ) -> Result<(), SimulatorError> {
168        let mut progress = NormalizationProgress::default();
169        let mut cycle_error = ErrorCode::DoubleSpend;
170
171        loop {
172            let tx_id = spend_bundle.name();
173            if self.mempool.contains_key(&tx_id) {
174                return Ok(());
175            }
176            if !progress.record(&spend_bundle) {
177                return Err(SimulatorError::Validation(cycle_error));
178            }
179
180            if let FastForwardResult::Rewritten(rewritten) =
181                self.fast_forward_settled_spends(&spend_bundle)
182            {
183                cycle_error = ErrorCode::DoubleSpend;
184                spend_bundle = *rewritten;
185                continue;
186            }
187
188            let validated = self.validate_bundle(spend_bundle)?;
189            if self.mempool_rejects(&validated) {
190                match self.fast_forward_mempool_spends(&validated) {
191                    FastForwardResult::Rewritten(rewritten) => {
192                        cycle_error = ErrorCode::MempoolConflict;
193                        spend_bundle = *rewritten;
194                        continue;
195                    }
196                    FastForwardResult::NoProgress => {
197                        return Err(SimulatorError::Validation(ErrorCode::MempoolConflict));
198                    }
199                }
200            }
201
202            return self.insert_mempool_item(tx_id, validated);
203        }
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use chia_bls::Signature;
210    use chia_protocol::{Coin, CoinSpend, Program};
211
212    use super::*;
213
214    #[test]
215    fn normalization_progress_detects_cycles_without_false_identity_matches() {
216        let coin = Coin::new([1; 32].into(), [2; 32].into(), 1);
217        let first = SpendBundle::new(
218            vec![CoinSpend::new(
219                coin,
220                Program::from(vec![1]),
221                Program::from(vec![2]),
222            )],
223            Signature::default(),
224        );
225        let second = SpendBundle::new(
226            vec![CoinSpend::new(
227                coin,
228                Program::from(vec![1]),
229                Program::from(vec![3]),
230            )],
231            Signature::default(),
232        );
233        let mut progress = NormalizationProgress::default();
234
235        assert!(progress.record(&first));
236        assert!(progress.record(&second));
237        assert!(!progress.record(&first));
238    }
239}