Skip to main content

chia_sdk_test/
full_node_simulator.rs

1use std::time::{SystemTime, UNIX_EPOCH};
2
3use bip39::Mnemonic;
4use chia_bls::{SecretKey, master_to_wallet_hardened};
5use chia_protocol::{BlockRecord, Bytes32, ClassgroupElement, Coin};
6use chia_puzzle_types::{DeriveSynthetic, standard::StandardArgs};
7use chia_sha2::Sha256;
8use hex_literal::hex;
9use indexmap::IndexMap;
10use rand::{Rng, SeedableRng};
11use rand_chacha::ChaCha8Rng;
12
13mod chain;
14mod fast_forward;
15mod push_tx;
16mod queries;
17mod state;
18#[cfg(feature = "serde")]
19mod state_dump;
20mod types;
21mod validation;
22
23#[cfg(test)]
24mod tests;
25
26use state::ChainState;
27use types::{SimBlock, SimCoinRecord, ValidatedBundle, ValidatedSpend};
28
29pub use types::{FullNodeSimulatorEvent, FullNodeSimulatorPushTxResponse};
30
31const BLOCK_REWARD_AMOUNT: u64 = 2_000_000_000_000;
32const PREFARM_WALLET_INDEX: u32 = 1;
33const SIMULATOR_GENESIS_CHALLENGE: Bytes32 = Bytes32::new(hex!(
34    "eb8c4d20b322be8d9fddbf9412016bdffe9a2901d7edb0e364e94266d0e095f7"
35));
36
37#[derive(Debug, Clone)]
38pub struct FullNodeSimulator {
39    rng: ChaCha8Rng,
40    state: ChainState,
41    orphaned_blocks: IndexMap<Bytes32, SimBlock>,
42    mempool: IndexMap<Bytes32, ValidatedBundle>,
43    farming_puzzle_hash: Bytes32,
44    master_secret_key: SecretKey,
45    prefarm_puzzle_hash: Bytes32,
46    node_id: Bytes32,
47    events: Vec<FullNodeSimulatorEvent>,
48}
49
50impl Default for FullNodeSimulator {
51    fn default() -> Self {
52        Self::with_seed(1337)
53    }
54}
55
56impl FullNodeSimulator {
57    pub fn new() -> Self {
58        Self::default()
59    }
60
61    pub fn with_seed(seed: u64) -> Self {
62        Self::with_secret_key_and_rng(
63            Self::secret_key_from_seed(seed),
64            ChaCha8Rng::seed_from_u64(seed),
65        )
66    }
67
68    pub fn with_secret_key(root_secret_key: SecretKey) -> Self {
69        let mut seed = [0; 32];
70        seed.copy_from_slice(&root_secret_key.to_bytes());
71        Self::with_secret_key_and_rng(root_secret_key, ChaCha8Rng::from_seed(seed))
72    }
73
74    fn with_secret_key_and_rng(root_secret_key: SecretKey, mut rng: ChaCha8Rng) -> Self {
75        let prefarm_secret_key =
76            master_to_wallet_hardened(&root_secret_key, PREFARM_WALLET_INDEX).derive_synthetic();
77        let prefarm_puzzle_hash =
78            StandardArgs::curry_tree_hash(prefarm_secret_key.public_key()).into();
79        let mut node_id = [0; 32];
80        rng.fill(&mut node_id);
81
82        let genesis_height = 1;
83        let genesis_timestamp = SystemTime::now()
84            .duration_since(UNIX_EPOCH)
85            .unwrap()
86            .as_secs();
87
88        let genesis_hash = Bytes32::default();
89        let prefarm_coins = vec![
90            Self::reward_coin(
91                genesis_hash,
92                genesis_height,
93                0,
94                prefarm_puzzle_hash,
95                18_375_000_000_000_000_000,
96            ),
97            Self::reward_coin(
98                genesis_hash,
99                genesis_height,
100                1,
101                prefarm_puzzle_hash,
102                2_625_000_000_000_000_000,
103            ),
104        ];
105        let genesis_record = Self::make_block_record(
106            genesis_hash,
107            Bytes32::default(),
108            genesis_height,
109            genesis_timestamp,
110            Bytes32::default(),
111            0,
112            0,
113            prefarm_puzzle_hash,
114            prefarm_coins.clone(),
115        );
116        let additions = prefarm_coins.iter().map(Coin::coin_id).collect::<Vec<_>>();
117        let mut coins = IndexMap::new();
118        for coin in prefarm_coins {
119            coins.insert(
120                coin.coin_id(),
121                SimCoinRecord {
122                    coin,
123                    coinbase: true,
124                    confirmed_block_index: genesis_height,
125                    spent_block_index: None,
126                    timestamp: genesis_timestamp,
127                },
128            );
129        }
130        let mut blocks = IndexMap::new();
131        blocks.insert(
132            genesis_hash,
133            SimBlock {
134                record: genesis_record,
135                additions: additions.clone(),
136                removals: Vec::new(),
137                spends: Vec::new(),
138                transactions: Vec::new(),
139                delta: state::BlockDelta {
140                    coins: additions
141                        .iter()
142                        .map(|coin_id| state::CoinChange {
143                            coin_id: *coin_id,
144                            before: None,
145                            after: coins.get(coin_id).copied(),
146                        })
147                        .collect(),
148                    ..state::BlockDelta::default()
149                },
150            },
151        );
152
153        Self {
154            rng,
155            state: ChainState::new(
156                genesis_height,
157                genesis_timestamp.saturating_add(1),
158                vec![genesis_hash],
159                blocks,
160                coins,
161                IndexMap::new(),
162                IndexMap::new(),
163            ),
164            orphaned_blocks: IndexMap::new(),
165            mempool: IndexMap::new(),
166            farming_puzzle_hash: prefarm_puzzle_hash,
167            master_secret_key: root_secret_key,
168            prefarm_puzzle_hash,
169            node_id: node_id.into(),
170            events: Vec::new(),
171        }
172    }
173
174    pub fn insert_coin(&mut self, coin: Coin) {
175        self.insert_coin_record(coin, false, self.state.height, self.state.next_timestamp);
176    }
177
178    pub fn new_coin(&mut self, puzzle_hash: Bytes32, amount: u64) -> Coin {
179        let mut parent_coin_info = [0; 32];
180        self.rng.fill(&mut parent_coin_info);
181        let coin = Coin::new(parent_coin_info.into(), puzzle_hash, amount);
182        self.insert_coin(coin);
183        coin
184    }
185
186    fn insert_coin_record(&mut self, coin: Coin, coinbase: bool, height: u32, timestamp: u64) {
187        self.state.insert_manual_coin(
188            coin.coin_id(),
189            SimCoinRecord {
190                coin,
191                coinbase,
192                confirmed_block_index: height,
193                spent_block_index: None,
194                timestamp,
195            },
196        );
197    }
198
199    fn secret_key_from_seed(seed: u64) -> SecretKey {
200        let mut rng = ChaCha8Rng::seed_from_u64(seed);
201        let entropy: [u8; 32] = rng.random();
202        let mnemonic = Mnemonic::from_entropy(&entropy).expect("32 bytes is valid BIP39 entropy");
203        SecretKey::from_seed(&mnemonic.to_seed(""))
204    }
205
206    fn reward_coin(
207        header_hash: Bytes32,
208        height: u32,
209        index: u8,
210        puzzle_hash: Bytes32,
211        amount: u64,
212    ) -> Coin {
213        Coin::new(
214            Self::reward_parent_id(header_hash, height, index),
215            puzzle_hash,
216            amount,
217        )
218    }
219
220    fn reward_parent_id(header_hash: Bytes32, height: u32, index: u8) -> Bytes32 {
221        let mut hasher = Sha256::new();
222        hasher.update(b"chia-sdk-full-node-simulator-reward");
223        hasher.update(header_hash.to_bytes());
224        hasher.update(height.to_be_bytes());
225        hasher.update([index]);
226        hasher.finalize().into()
227    }
228
229    #[allow(clippy::too_many_arguments)]
230    fn make_block_record(
231        header_hash: Bytes32,
232        prev_hash: Bytes32,
233        height: u32,
234        timestamp: u64,
235        prev_transaction_block_hash: Bytes32,
236        fees: u64,
237        prev_transaction_block_height: u32,
238        farming_puzzle_hash: Bytes32,
239        reward_claims_incorporated: Vec<Coin>,
240    ) -> BlockRecord {
241        BlockRecord::new(
242            header_hash,
243            prev_hash,
244            height,
245            u128::from(height),
246            u128::from(height),
247            0,
248            ClassgroupElement::default(),
249            None,
250            header_hash,
251            header_hash,
252            1,
253            farming_puzzle_hash,
254            farming_puzzle_hash,
255            0,
256            15,
257            false,
258            prev_transaction_block_height,
259            Some(timestamp),
260            Some(prev_transaction_block_hash),
261            Some(fees),
262            Some(reward_claims_incorporated),
263            None,
264            None,
265            None,
266            None,
267        )
268    }
269}