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}