chia_sdk_test/full_node_simulator/
push_tx.rs1use 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}