1use std::{
3 collections::BTreeMap,
4 fs::{self, File},
5 io::{self, Error, ErrorKind, Write},
6 path::Path,
7 time::SystemTime,
8 u64,
9};
10
11use crate::{constants, coreth::genesis as coreth_genesis, key};
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
18pub struct Genesis {
19 #[serde(rename = "networkID")]
20 pub network_id: u32,
21
22 #[serde(rename = "allocations", skip_serializing_if = "Option::is_none")]
23 pub allocations: Option<Vec<Allocation>>,
24
25 #[serde(rename = "startTime", skip_serializing_if = "Option::is_none")]
27 pub start_time: Option<u64>,
28 #[serde(
30 rename = "initialStakeDuration",
31 skip_serializing_if = "Option::is_none"
32 )]
33 pub initial_stake_duration: Option<u64>,
34 #[serde(
35 rename = "initialStakeDurationOffset",
36 skip_serializing_if = "Option::is_none"
37 )]
38 pub initial_stake_duration_offset: Option<u64>,
39 #[serde(rename = "initialStakedFunds", skip_serializing_if = "Option::is_none")]
43 pub initial_staked_funds: Option<Vec<String>>,
44 #[serde(rename = "initialStakers", skip_serializing_if = "Option::is_none")]
48 pub initial_stakers: Option<Vec<Staker>>,
49
50 #[serde(rename = "cChainGenesis")]
51 pub c_chain_genesis: coreth_genesis::Genesis,
52
53 #[serde(rename = "message", skip_serializing_if = "Option::is_none")]
54 pub message: Option<String>,
55}
56
57#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
61struct GenesisFile {
62 #[serde(rename = "networkID")]
63 network_id: u32,
64 #[serde(rename = "allocations", skip_serializing_if = "Option::is_none")]
65 allocations: Option<Vec<Allocation>>,
66 #[serde(rename = "startTime", skip_serializing_if = "Option::is_none")]
67 start_time: Option<u64>,
68 #[serde(
69 rename = "initialStakeDuration",
70 skip_serializing_if = "Option::is_none"
71 )]
72 initial_stake_duration: Option<u64>,
73 #[serde(
74 rename = "initialStakeDurationOffset",
75 skip_serializing_if = "Option::is_none"
76 )]
77 initial_stake_duration_offset: Option<u64>,
78 #[serde(rename = "initialStakedFunds", skip_serializing_if = "Option::is_none")]
80 initial_staked_funds: Option<Vec<String>>,
81 #[serde(rename = "initialStakers", skip_serializing_if = "Option::is_none")]
82 initial_stakers: Option<Vec<Staker>>,
83
84 #[serde(rename = "cChainGenesis")]
85 c_chain_genesis: String,
86
87 #[serde(rename = "message", skip_serializing_if = "Option::is_none")]
88 message: Option<String>,
89}
90
91pub const DEFAULT_INITIAL_STAKE_DURATION: u64 = 31536000; pub const DEFAULT_INITIAL_STAKE_DURATION_OFFSET: u64 = 5400; impl Default for Genesis {
95 fn default() -> Self {
96 let now_unix = SystemTime::now()
97 .duration_since(SystemTime::UNIX_EPOCH)
98 .expect("unexpected None duration_since")
99 .as_secs();
100
101 let start_time = now_unix;
103
104 Self {
105 network_id: constants::DEFAULT_CUSTOM_NETWORK_ID,
106 allocations: Some(Vec::new()),
107 start_time: Some(start_time),
108 initial_stake_duration: Some(DEFAULT_INITIAL_STAKE_DURATION),
109 initial_stake_duration_offset: Some(DEFAULT_INITIAL_STAKE_DURATION_OFFSET),
110 initial_staked_funds: Some(Vec::new()),
111 initial_stakers: None,
112 c_chain_genesis: coreth_genesis::Genesis::default(),
113 message: Some(String::new()),
114 }
115 }
116}
117
118impl Genesis {
119 pub fn new<T: key::secp256k1::ReadOnly>(network_id: u32, seed_keys: &[T]) -> io::Result<Self> {
121 let max_total_alloc = u64::MAX;
123 let total_keys = seed_keys.len();
124 let alloc_per_key = if total_keys > 0 {
125 max_total_alloc / total_keys as u64
126 } else {
127 0u64
128 };
129 let xp_alloc_per_key = (alloc_per_key / 10) * 4;
131 log::info!("allocate {} for each key in X/P-chain", xp_alloc_per_key);
132
133 let max_total_alloc = i128::MAX;
135 let alloc_per_key = if total_keys > 0 {
136 max_total_alloc / total_keys as i128
137 } else {
138 0i128
139 };
140 let c_alloc_per_key = alloc_per_key / 2;
142 log::info!("allocate {} for each key in C-chain", c_alloc_per_key);
143
144 let default_c_alloc = coreth_genesis::AllocAccount {
146 balance: primitive_types::U256::from(c_alloc_per_key),
147 ..Default::default()
148 };
149
150 let initial_staked_funds = vec![seed_keys[seed_keys.len() - 1]
153 .hrp_address(network_id, "X")
154 .unwrap()];
155
156 let mut xp_allocs: Vec<Allocation> = Vec::new();
157 let mut c_allocs = BTreeMap::new();
158
159 for k in seed_keys.iter() {
160 let xp_alloc = Allocation {
162 eth_addr: Some(k.eth_address()),
163 avax_addr: Some(k.hrp_address(network_id, "X").unwrap()),
164 initial_amount: Some(xp_alloc_per_key),
165 unlock_schedule: Some(vec![LockedAmount {
166 amount: Some(xp_alloc_per_key),
167 ..Default::default()
168 }]),
169 };
170 xp_allocs.push(xp_alloc);
171
172 c_allocs.insert(
173 k.eth_address().trim_start_matches("0x").to_string(),
174 default_c_alloc.clone(),
175 );
176 }
177
178 let c_chain_genesis = coreth_genesis::Genesis {
181 alloc: Some(c_allocs),
182 ..Default::default()
183 };
184
185 Ok(Self {
186 network_id,
187 initial_staked_funds: Some(initial_staked_funds),
188 allocations: Some(xp_allocs),
189 c_chain_genesis,
190 ..Default::default()
191 })
192 }
193
194 pub fn sync(&self, file_path: &str) -> io::Result<()> {
197 log::info!("syncing genesis to '{}'", file_path);
198 let path = Path::new(file_path);
199 if let Some(parent_dir) = path.parent() {
200 log::info!("creating parent dir '{}'", parent_dir.display());
201 fs::create_dir_all(parent_dir)?;
202 }
203
204 let c_chain_genesis = self.c_chain_genesis.encode_json()?;
205 let genesis_file = GenesisFile {
206 network_id: self.network_id,
207 allocations: self.allocations.clone(),
208 start_time: self.start_time,
209 initial_stake_duration: self.initial_stake_duration,
210 initial_stake_duration_offset: self.initial_stake_duration_offset,
211 initial_staked_funds: self.initial_staked_funds.clone(),
212 initial_stakers: self.initial_stakers.clone(),
213
214 c_chain_genesis,
216
217 message: self.message.clone(),
218 };
219
220 let d = serde_json::to_vec(&genesis_file)
221 .map_err(|e| Error::new(ErrorKind::Other, format!("failed to serialize JSON {}", e)))?;
222
223 let mut f = File::create(file_path)?;
224 f.write_all(&d)?;
225
226 Ok(())
227 }
228
229 pub fn load(file_path: &str) -> io::Result<Self> {
230 log::info!("loading genesis from {}", file_path);
231
232 if !Path::new(file_path).exists() {
233 return Err(Error::new(
234 ErrorKind::NotFound,
235 format!("file {} does not exists", file_path),
236 ));
237 }
238
239 let f = File::open(file_path).map_err(|e| {
240 Error::new(
241 ErrorKind::Other,
242 format!("failed to open {} ({})", file_path, e),
243 )
244 })?;
245
246 let genesis_file: GenesisFile = serde_json::from_reader(f)
248 .map_err(|e| Error::new(ErrorKind::InvalidInput, format!("invalid JSON: {}", e)))?;
249
250 let c_chain_genesis: coreth_genesis::Genesis =
252 serde_json::from_str(&genesis_file.c_chain_genesis)
253 .map_err(|e| Error::new(ErrorKind::InvalidInput, format!("invalid JSON: {}", e)))?;
254
255 let genesis = Genesis {
256 network_id: genesis_file.network_id,
257 allocations: genesis_file.allocations.clone(),
258 start_time: genesis_file.start_time,
259 initial_stake_duration: genesis_file.initial_stake_duration,
260 initial_stake_duration_offset: genesis_file.initial_stake_duration_offset,
261 initial_staked_funds: genesis_file.initial_staked_funds.clone(),
262 initial_stakers: genesis_file.initial_stakers.clone(),
263
264 c_chain_genesis,
266
267 message: genesis_file.message,
268 };
269 Ok(genesis)
270 }
271}
272
273#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
275pub struct Allocation {
276 #[serde(rename = "avaxAddr", skip_serializing_if = "Option::is_none")]
277 pub avax_addr: Option<String>,
278 #[serde(rename = "ethAddr", skip_serializing_if = "Option::is_none")]
281 pub eth_addr: Option<String>,
282 #[serde(rename = "initialAmount", skip_serializing_if = "Option::is_none")]
288 pub initial_amount: Option<u64>,
289 #[serde(rename = "unlockSchedule", skip_serializing_if = "Option::is_none")]
290 pub unlock_schedule: Option<Vec<LockedAmount>>,
291}
292
293pub const DEFAULT_INITIAL_AMOUNT_X_CHAIN: u64 = 300000000000000000;
299
300pub const DEFAULT_LOCKED_AMOUNT_P_CHAIN: u64 = 200000000000000000;
306
307impl Default for Allocation {
308 fn default() -> Self {
309 let unlock_empty = LockedAmount::default();
310 Self {
311 avax_addr: None,
312 eth_addr: None,
313 initial_amount: Some(DEFAULT_INITIAL_AMOUNT_X_CHAIN),
314 unlock_schedule: Some(vec![unlock_empty]),
315 }
316 }
317}
318
319#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
321pub struct LockedAmount {
322 #[serde(rename = "amount", skip_serializing_if = "Option::is_none")]
329 pub amount: Option<u64>,
330 #[serde(rename = "locktime", skip_serializing_if = "Option::is_none")]
332 pub locktime: Option<u64>,
333}
334
335impl Default for LockedAmount {
336 fn default() -> Self {
337 Self {
345 amount: Some(DEFAULT_LOCKED_AMOUNT_P_CHAIN),
346 locktime: None, }
348 }
349}
350
351#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
353pub struct Staker {
354 #[serde(rename = "nodeID", skip_serializing_if = "Option::is_none")]
355 pub node_id: Option<String>,
356 #[serde(rename = "rewardAddress", skip_serializing_if = "Option::is_none")]
357 pub reward_address: Option<String>,
358 #[serde(rename = "delegationFee", skip_serializing_if = "Option::is_none")]
359 pub delegation_fee: Option<u32>,
360}
361
362pub const DEFAULT_DELEGATION_FEE: u32 = 62500;
363
364impl Default for Staker {
365 fn default() -> Self {
366 Self {
367 node_id: None,
368 reward_address: None,
369 delegation_fee: Some(DEFAULT_DELEGATION_FEE),
370 }
371 }
372}
373
374#[test]
375fn test_genesis() {
376 let _ = env_logger::builder()
377 .filter_level(log::LevelFilter::Info)
378 .is_test(true)
379 .try_init();
380
381 use rust_embed::RustEmbed;
382 #[derive(RustEmbed)]
383 #[folder = "artifacts/"]
384 #[prefix = "artifacts/"]
385 struct Asset;
386 let genesis_json = Asset::get("artifacts/sample.genesis.json").unwrap();
387 let genesis_json_contents = std::str::from_utf8(genesis_json.data.as_ref()).unwrap();
388 let mut f = tempfile::NamedTempFile::new().unwrap();
389 f.write_all(genesis_json_contents.as_bytes()).unwrap();
390 let genesis_file_path = f.path().to_str().unwrap();
391 let original_genesis = Genesis::load(genesis_file_path).unwrap();
392
393 let mut alloc = BTreeMap::new();
394 alloc.insert(
395 String::from("8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"),
396 coreth_genesis::AllocAccount {
397 code: None,
398 storage: None,
399 balance: primitive_types::U256::from_str_radix("0x295BE96E64066972000000", 16).unwrap(),
400 mcbalance: None,
401 nonce: None,
402 },
403 );
404 let genesis = Genesis {
405 network_id: 1337,
406
407 allocations: Some(vec![
408 Allocation {
409 eth_addr: Some(String::from("0xb3d82b1367d362de99ab59a658165aff520cbd4d")),
410 avax_addr: Some(String::from(
411 "X-custom1g65uqn6t77p656w64023nh8nd9updzmxwd59gh",
412 )),
413 initial_amount: Some(0),
414 unlock_schedule: Some(vec![LockedAmount {
415 amount: Some(10000000000000000),
416 locktime: Some(1633824000),
417 }]),
418 },
419 Allocation {
420 eth_addr: Some(String::from("0xb3d82b1367d362de99ab59a658165aff520cbd4d")),
421 avax_addr: Some(String::from(
422 "X-custom18jma8ppw3nhx5r4ap8clazz0dps7rv5u9xde7p",
423 )),
424 initial_amount: Some(300000000000000000),
425 unlock_schedule: Some(vec![
426 LockedAmount {
427 amount: Some(20000000000000000),
428 locktime: None,
429 },
430 LockedAmount {
431 amount: Some(10000000000000000),
432 locktime: Some(1633824000),
433 },
434 ]),
435 },
436 Allocation {
437 eth_addr: Some(String::from("0xb3d82b1367d362de99ab59a658165aff520cbd4d")),
438 avax_addr: Some(String::from(
439 "X-custom16045mxr3s2cjycqe2xfluk304xv3ezhkhsvkpr",
440 )),
441 initial_amount: Some(10000000000000000),
442 unlock_schedule: Some(vec![LockedAmount {
443 amount: Some(10000000000000000),
444 locktime: Some(1633824000),
445 }]),
446 },
447 ]),
448
449 start_time: Some(1630987200),
450 initial_stake_duration: Some(31536000),
451 initial_stake_duration_offset: Some(5400),
452 initial_staked_funds: Some(vec![String::from(
453 "X-custom1g65uqn6t77p656w64023nh8nd9updzmxwd59gh",
454 )]),
455 initial_stakers: Some(vec![
456 Staker {
457 node_id: Some(String::from("NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg")),
458 reward_address: Some(String::from(
459 "X-custom18jma8ppw3nhx5r4ap8clazz0dps7rv5u9xde7p",
460 )),
461 delegation_fee: Some(1000000),
462 },
463 Staker {
464 node_id: Some(String::from("NodeID-MFrZFVCXPv5iCn6M9K6XduxGTYp891xXZ")),
465 reward_address: Some(String::from(
466 "X-custom18jma8ppw3nhx5r4ap8clazz0dps7rv5u9xde7p",
467 )),
468 delegation_fee: Some(500000),
469 },
470 Staker {
471 node_id: Some(String::from("NodeID-NFBbbJ4qCmNaCzeW7sxErhvWqvEQMnYcN")),
472 reward_address: Some(String::from(
473 "X-custom18jma8ppw3nhx5r4ap8clazz0dps7rv5u9xde7p",
474 )),
475 delegation_fee: Some(250000),
476 },
477 Staker {
478 node_id: Some(String::from("NodeID-GWPcbFJZFfZreETSoWjPimr846mXEKCtu")),
479 reward_address: Some(String::from(
480 "X-custom18jma8ppw3nhx5r4ap8clazz0dps7rv5u9xde7p",
481 )),
482 delegation_fee: Some(125000),
483 },
484 Staker {
485 node_id: Some(String::from("NodeID-P7oB2McjBGgW2NXXWVYjV8JEDFoW9xDE5")),
486 reward_address: Some(String::from(
487 "X-custom18jma8ppw3nhx5r4ap8clazz0dps7rv5u9xde7p",
488 )),
489 delegation_fee: Some(62500),
490 },
491 ]),
492
493 c_chain_genesis: coreth_genesis::Genesis {
494 config: Some(coreth_genesis::ChainConfig {
495 chain_id: Some(43112),
496
497 homestead_block: Some(0),
498
499 dao_fork_block: Some(0),
500 dao_fork_support: Some(true),
501
502 eip150_block: Some(0),
503 eip150_hash: Some(String::from(
504 "0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0",
505 )),
506
507 eip155_block: Some(0),
508 eip158_block: Some(0),
509
510 byzantium_block: Some(0),
511 constantinople_block: Some(0),
512 petersburg_block: Some(0),
513 istanbul_block: Some(0),
514 muir_glacier_block: Some(0),
515
516 apricot_phase1_block_timestamp: Some(0),
517 apricot_phase2_block_timestamp: Some(0),
518 apricot_phase3_block_timestamp: Some(0),
519 apricot_phase4_block_timestamp: Some(0),
520 apricot_phase5_block_timestamp: Some(0),
521 apricot_phase_pre6_block_timestamp: Some(0),
522 apricot_phase6_block_timestamp: Some(0),
523 apricot_phase_post6_block_timestamp: Some(0),
524 banff_block_timestamp: Some(0),
525 cortina_block_timestamp: Some(0),
526 }),
527 nonce: primitive_types::U256::zero(),
528 timestamp: primitive_types::U256::zero(),
529 extra_data: Some(String::from("0x00")),
530 gas_limit: primitive_types::U256::from_str_radix("0x5f5e100", 16).unwrap(),
531 difficulty: primitive_types::U256::zero(),
532 mix_hash: Some(String::from(
533 "0x0000000000000000000000000000000000000000000000000000000000000000",
534 )),
535 coinbase: Some(String::from("0x0000000000000000000000000000000000000000")),
536 alloc: Some(alloc),
537
538 number: primitive_types::U256::zero(),
539 gas_used: primitive_types::U256::zero(),
540 parent_hash: Some(String::from(
541 "0x0000000000000000000000000000000000000000000000000000000000000000",
542 )),
543 base_fee: None,
544 },
545
546 message: Some(String::from("{{ fun_quote }}")),
547 };
548 assert_eq!(original_genesis, genesis);
549
550 let p = random_manager::tmp_path(10, Some(".json")).unwrap();
551 genesis.sync(&p).unwrap();
552 let genesis_loaded = Genesis::load(&p).unwrap();
553 assert_eq!(genesis_loaded, genesis);
554 assert_eq!(genesis_loaded, original_genesis);
555
556 let d = fs::read_to_string(&p).unwrap();
557 log::info!("{}", d);
558}