chia_sdk_test/
simulator.rs

1use std::collections::HashSet;
2
3use chia_bls::SecretKey;
4use chia_consensus::validation_error::ErrorCode;
5use chia_protocol::{Bytes32, Coin, CoinSpend, CoinState, Program, SpendBundle};
6use chia_sdk_types::TESTNET11_CONSTANTS;
7use clvmr::ENABLE_KECCAK_OPS_OUTSIDE_GUARD;
8use indexmap::{IndexMap, IndexSet, indexset};
9use rand::{Rng, SeedableRng};
10use rand_chacha::ChaCha8Rng;
11
12use crate::{
13    BlsPair, BlsPairWithCoin, SimulatorError, sign_transaction, validate_clvm_and_signature,
14};
15
16mod config;
17mod data;
18
19pub use config::*;
20
21use data::SimulatorData;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Simulator {
25    config: SimulatorConfig,
26    data: SimulatorData,
27}
28
29impl Default for Simulator {
30    fn default() -> Self {
31        Self::new()
32    }
33}
34
35impl Simulator {
36    pub fn new() -> Self {
37        Self::with_config(SimulatorConfig::default())
38    }
39
40    pub fn with_config(config: SimulatorConfig) -> Self {
41        Self {
42            config,
43            data: SimulatorData::new(ChaCha8Rng::seed_from_u64(config.seed)),
44        }
45    }
46
47    #[cfg(feature = "serde")]
48    pub fn serialize(&self) -> Result<Vec<u8>, bincode::error::EncodeError> {
49        bincode::serde::encode_to_vec(&self.data, bincode::config::standard())
50    }
51
52    #[cfg(feature = "serde")]
53    pub fn deserialize_with_config(
54        data: &[u8],
55        config: SimulatorConfig,
56    ) -> Result<Self, bincode::error::DecodeError> {
57        let data: SimulatorData =
58            bincode::serde::decode_from_slice(data, bincode::config::standard())?.0;
59        Ok(Self { config, data })
60    }
61
62    #[cfg(feature = "serde")]
63    pub fn deserialize(data: &[u8]) -> Result<Self, bincode::error::DecodeError> {
64        Self::deserialize_with_config(data, SimulatorConfig::default())
65    }
66
67    pub fn height(&self) -> u32 {
68        self.data.height
69    }
70
71    pub fn next_timestamp(&self) -> u64 {
72        self.data.next_timestamp
73    }
74
75    pub fn header_hash(&self) -> Bytes32 {
76        self.data.header_hashes.last().copied().unwrap()
77    }
78
79    pub fn header_hash_of(&self, height: u32) -> Option<Bytes32> {
80        self.data.header_hashes.get(height as usize).copied()
81    }
82
83    pub fn insert_coin(&mut self, coin: Coin) {
84        let coin_state = CoinState::new(coin, None, Some(self.data.height));
85        self.data.coin_states.insert(coin.coin_id(), coin_state);
86    }
87
88    pub fn new_coin(&mut self, puzzle_hash: Bytes32, amount: u64) -> Coin {
89        let mut parent_coin_info = [0; 32];
90        self.data.rng.fill(&mut parent_coin_info);
91        let coin = Coin::new(parent_coin_info.into(), puzzle_hash, amount);
92        self.insert_coin(coin);
93        coin
94    }
95
96    pub fn bls(&mut self, amount: u64) -> BlsPairWithCoin {
97        let pair = BlsPair::new(self.data.rng.random());
98        let coin = self.new_coin(pair.puzzle_hash, amount);
99        BlsPairWithCoin::new(pair, coin)
100    }
101
102    pub fn set_next_timestamp(&mut self, time: u64) -> Result<(), SimulatorError> {
103        if self.data.height > 0
104            && let Some(last_block_timestamp) =
105                self.data.block_timestamps.get(&(self.data.height - 1))
106            && time < *last_block_timestamp
107        {
108            return Err(SimulatorError::Validation(ErrorCode::TimestampTooFarInPast));
109        }
110        self.data.next_timestamp = time;
111
112        Ok(())
113    }
114
115    pub fn pass_time(&mut self, time: u64) {
116        self.data.next_timestamp += time;
117    }
118
119    pub fn hint_coin(&mut self, coin_id: Bytes32, hint: Bytes32) {
120        self.data
121            .hinted_coins
122            .entry(hint)
123            .or_default()
124            .insert(coin_id);
125    }
126
127    pub fn coin_state(&self, coin_id: Bytes32) -> Option<CoinState> {
128        self.data.coin_states.get(&coin_id).copied()
129    }
130
131    pub fn children(&self, coin_id: Bytes32) -> Vec<CoinState> {
132        self.data
133            .coin_states
134            .values()
135            .filter(move |cs| cs.coin.parent_coin_info == coin_id)
136            .copied()
137            .collect()
138    }
139
140    pub fn hinted_coins(&self, hint: Bytes32) -> Vec<Bytes32> {
141        self.data
142            .hinted_coins
143            .get(&hint)
144            .into_iter()
145            .flatten()
146            .copied()
147            .collect()
148    }
149
150    pub fn puzzle_reveal(&self, coin_id: Bytes32) -> Option<Program> {
151        self.data
152            .coin_spends
153            .get(&coin_id)
154            .map(|spend| spend.puzzle_reveal.clone())
155    }
156
157    pub fn solution(&self, coin_id: Bytes32) -> Option<Program> {
158        self.data
159            .coin_spends
160            .get(&coin_id)
161            .map(|spend| spend.solution.clone())
162    }
163
164    pub fn puzzle_and_solution(&self, coin_id: Bytes32) -> Option<(Program, Program)> {
165        self.data
166            .coin_spends
167            .get(&coin_id)
168            .map(|spend| (spend.puzzle_reveal.clone(), spend.solution.clone()))
169    }
170
171    pub fn coin_spend(&self, coin_id: Bytes32) -> Option<CoinSpend> {
172        self.data.coin_spends.get(&coin_id).cloned()
173    }
174
175    pub fn spend_coins(
176        &mut self,
177        coin_spends: Vec<CoinSpend>,
178        secret_keys: &[SecretKey],
179    ) -> Result<IndexMap<Bytes32, CoinState>, SimulatorError> {
180        let signature = sign_transaction(&coin_spends, secret_keys)?;
181        self.new_transaction(SpendBundle::new(coin_spends, signature))
182    }
183
184    /// Processes a spend bunndle and returns the updated coin states.
185    pub fn new_transaction(
186        &mut self,
187        spend_bundle: SpendBundle,
188    ) -> Result<IndexMap<Bytes32, CoinState>, SimulatorError> {
189        if spend_bundle.coin_spends.is_empty() {
190            return Err(SimulatorError::Validation(ErrorCode::InvalidSpendBundle));
191        }
192
193        let conds = validate_clvm_and_signature(
194            &spend_bundle,
195            11_000_000_000 / 2,
196            &TESTNET11_CONSTANTS,
197            ENABLE_KECCAK_OPS_OUTSIDE_GUARD,
198        )
199        .map_err(SimulatorError::Validation)?;
200
201        let puzzle_hashes: HashSet<Bytes32> =
202            conds.spends.iter().map(|spend| spend.puzzle_hash).collect();
203
204        let bundle_puzzle_hashes: HashSet<Bytes32> = spend_bundle
205            .coin_spends
206            .iter()
207            .map(|cs| cs.coin.puzzle_hash)
208            .collect();
209
210        if puzzle_hashes != bundle_puzzle_hashes {
211            return Err(SimulatorError::Validation(ErrorCode::InvalidSpendBundle));
212        }
213
214        let mut removed_coins = IndexMap::new();
215        let mut added_coins = IndexMap::new();
216        let mut added_hints = IndexMap::new();
217        let mut coin_spends = IndexMap::new();
218
219        if self.data.height < conds.height_absolute {
220            return Err(SimulatorError::Validation(
221                ErrorCode::AssertHeightAbsoluteFailed,
222            ));
223        }
224
225        if self.data.next_timestamp < conds.seconds_absolute {
226            return Err(SimulatorError::Validation(
227                ErrorCode::AssertSecondsAbsoluteFailed,
228            ));
229        }
230
231        if let Some(height) = conds.before_height_absolute
232            && height < self.data.height
233        {
234            return Err(SimulatorError::Validation(
235                ErrorCode::AssertBeforeHeightAbsoluteFailed,
236            ));
237        }
238
239        if let Some(seconds) = conds.before_seconds_absolute
240            && seconds < self.data.next_timestamp
241        {
242            return Err(SimulatorError::Validation(
243                ErrorCode::AssertBeforeSecondsAbsoluteFailed,
244            ));
245        }
246
247        for coin_spend in spend_bundle.coin_spends {
248            coin_spends.insert(coin_spend.coin.coin_id(), coin_spend);
249        }
250
251        // Calculate additions and removals.
252        for spend in &conds.spends {
253            for new_coin in &spend.create_coin {
254                let coin = Coin::new(spend.coin_id, new_coin.0, new_coin.1);
255
256                added_coins.insert(
257                    coin.coin_id(),
258                    CoinState::new(coin, None, Some(self.data.height)),
259                );
260
261                let Some(hint) = new_coin.2.clone() else {
262                    continue;
263                };
264
265                if hint.len() != 32 {
266                    continue;
267                }
268
269                added_hints
270                    .entry(Bytes32::try_from(hint).unwrap())
271                    .or_insert_with(IndexSet::new)
272                    .insert(coin.coin_id());
273            }
274
275            let coin = Coin::new(spend.parent_id, spend.puzzle_hash, spend.coin_amount);
276
277            let coin_state = self
278                .data
279                .coin_states
280                .get(&spend.coin_id)
281                .copied()
282                .unwrap_or(CoinState::new(coin, None, Some(self.data.height)));
283
284            if let Some(relative_height) = spend.height_relative {
285                let Some(created_height) = coin_state.created_height else {
286                    return Err(SimulatorError::Validation(
287                        ErrorCode::EphemeralRelativeCondition,
288                    ));
289                };
290
291                if self.data.height < created_height + relative_height {
292                    return Err(SimulatorError::Validation(
293                        ErrorCode::AssertHeightRelativeFailed,
294                    ));
295                }
296            }
297
298            if let Some(relative_seconds) = spend.seconds_relative {
299                let Some(created_height) = coin_state.created_height else {
300                    return Err(SimulatorError::Validation(
301                        ErrorCode::EphemeralRelativeCondition,
302                    ));
303                };
304                let Some(created_timestamp) = self.data.block_timestamps.get(&created_height)
305                else {
306                    return Err(SimulatorError::Validation(
307                        ErrorCode::EphemeralRelativeCondition,
308                    ));
309                };
310
311                if self.data.next_timestamp < created_timestamp + relative_seconds {
312                    return Err(SimulatorError::Validation(
313                        ErrorCode::AssertSecondsRelativeFailed,
314                    ));
315                }
316            }
317
318            if let Some(relative_height) = spend.before_height_relative {
319                let Some(created_height) = coin_state.created_height else {
320                    return Err(SimulatorError::Validation(
321                        ErrorCode::EphemeralRelativeCondition,
322                    ));
323                };
324
325                if created_height + relative_height < self.data.height {
326                    return Err(SimulatorError::Validation(
327                        ErrorCode::AssertBeforeHeightRelativeFailed,
328                    ));
329                }
330            }
331
332            if let Some(relative_seconds) = spend.before_seconds_relative {
333                let Some(created_height) = coin_state.created_height else {
334                    return Err(SimulatorError::Validation(
335                        ErrorCode::EphemeralRelativeCondition,
336                    ));
337                };
338                let Some(created_timestamp) = self.data.block_timestamps.get(&created_height)
339                else {
340                    return Err(SimulatorError::Validation(
341                        ErrorCode::EphemeralRelativeCondition,
342                    ));
343                };
344
345                if created_timestamp + relative_seconds < self.data.next_timestamp {
346                    return Err(SimulatorError::Validation(
347                        ErrorCode::AssertBeforeSecondsRelativeFailed,
348                    ));
349                }
350            }
351
352            removed_coins.insert(spend.coin_id, coin_state);
353        }
354
355        // Validate removals.
356        for (coin_id, coin_state) in &mut removed_coins {
357            let height = self.data.height;
358
359            if !self.data.coin_states.contains_key(coin_id) && !added_coins.contains_key(coin_id) {
360                return Err(SimulatorError::Validation(ErrorCode::UnknownUnspent));
361            }
362
363            if coin_state.spent_height.is_some() {
364                return Err(SimulatorError::Validation(ErrorCode::DoubleSpend));
365            }
366
367            coin_state.spent_height = Some(height);
368        }
369
370        // Update the coin data.
371        let mut updates = added_coins.clone();
372        updates.extend(removed_coins);
373
374        self.create_block();
375
376        self.data.coin_states.extend(updates.clone());
377
378        if self.config.save_hints {
379            for (hint, coins) in added_hints {
380                self.data
381                    .hinted_coins
382                    .entry(hint)
383                    .or_default()
384                    .extend(coins);
385            }
386        }
387
388        if self.config.save_spends {
389            self.data.coin_spends.extend(coin_spends);
390        }
391
392        Ok(updates)
393    }
394
395    pub fn lookup_coin_ids(&self, coin_ids: &IndexSet<Bytes32>) -> Vec<CoinState> {
396        coin_ids
397            .iter()
398            .filter_map(|coin_id| self.data.coin_states.get(coin_id).copied())
399            .collect()
400    }
401
402    pub fn lookup_puzzle_hashes(
403        &self,
404        puzzle_hashes: IndexSet<Bytes32>,
405        include_hints: bool,
406    ) -> Vec<CoinState> {
407        let mut coin_states = IndexMap::new();
408
409        for (coin_id, coin_state) in &self.data.coin_states {
410            if puzzle_hashes.contains(&coin_state.coin.puzzle_hash) {
411                coin_states.insert(*coin_id, self.data.coin_states[coin_id]);
412            }
413        }
414
415        if include_hints {
416            for puzzle_hash in puzzle_hashes {
417                if let Some(hinted_coins) = self.data.hinted_coins.get(&puzzle_hash) {
418                    for coin_id in hinted_coins {
419                        coin_states.insert(*coin_id, self.data.coin_states[coin_id]);
420                    }
421                }
422            }
423        }
424
425        coin_states.into_values().collect()
426    }
427
428    pub fn unspent_coins(&self, puzzle_hash: Bytes32, include_hints: bool) -> Vec<Coin> {
429        self.lookup_puzzle_hashes(indexset![puzzle_hash], include_hints)
430            .iter()
431            .filter(|cs| cs.spent_height.is_none())
432            .map(|cs| cs.coin)
433            .collect()
434    }
435
436    pub fn create_block(&mut self) {
437        let mut header_hash = [0; 32];
438        self.data.rng.fill(&mut header_hash);
439        self.data.header_hashes.push(header_hash.into());
440        self.data
441            .block_timestamps
442            .insert(self.data.height, self.data.next_timestamp);
443
444        self.data.height += 1;
445        self.data.next_timestamp += 1;
446    }
447}