Skip to main content

avalanche_types/avalanchego/
genesis.rs

1//! AvalancheGo network configuration.
2use 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/// Represents Avalanche network genesis configuration.
15/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/genesis#Config>
16/// ref. <https://serde.rs/container-attrs.html>
17#[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    /// Unix time for start time.
26    #[serde(rename = "startTime", skip_serializing_if = "Option::is_none")]
27    pub start_time: Option<u64>,
28    /// Number of seconds to stake for the initial stakers.
29    #[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    /// MUST BE come from "initial_stakers".
40    /// MUST BE the list of X-chain addresses.
41    /// Initial staked funds cannot be empty.
42    #[serde(rename = "initialStakedFunds", skip_serializing_if = "Option::is_none")]
43    pub initial_staked_funds: Option<Vec<String>>,
44    /// MUST BE non-empty for an existing network.
45    /// Non-anchor nodes request "GetAcceptedFrontier" from initial stakers
46    /// (not from specified anchor nodes).
47    #[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/// All of the P-chain assets owned by "initialStakedFunds" are evenly
58/// distributed over the "initialStakers".
59/// The P-chain assets are determined by the "unlockSchedule".
60#[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    /// Initially staked funds are immediately locked for "initial_stake_duration".
79    #[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; // 1 year
92pub const DEFAULT_INITIAL_STAKE_DURATION_OFFSET: u64 = 5400; // 1.5 hour
93
94impl 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        // TODO: 5-hr in the past to unlock "Staking" immediately?
102        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    /// Creates a new Genesis object with "keys" number of generated pre-funded keys.
120    pub fn new<T: key::secp256k1::ReadOnly>(network_id: u32, seed_keys: &[T]) -> io::Result<Self> {
121        // maximize total supply
122        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        // keep 40%, allow more room for transfers
130        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        // maximize total supply
134        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        // divide by 2, allow more room for transfers
141        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        // allocation for C-chain
145        let default_c_alloc = coreth_genesis::AllocAccount {
146            balance: primitive_types::U256::from(c_alloc_per_key),
147            ..Default::default()
148        };
149
150        // "initial_staked_funds" addresses use all P-chain balance
151        // so keep the remaining balance for other keys than "last" key
152        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            // allocation for X/P-chain
161            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        // make sure to use different network ID than "local" network ID
179        // ref. https://github.com/ava-labs/avalanche-ops/issues/8
180        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    /// Saves the current configuration to disk
195    /// and overwrites the file.
196    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            // the avalanchego can only read string-format c-chain genesis
215            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        // load as it is
247        let genesis_file: GenesisFile = serde_json::from_reader(f)
248            .map_err(|e| Error::new(ErrorKind::InvalidInput, format!("invalid JSON: {}", e)))?;
249
250        // make genesis strictly typed
251        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            // the avalanchego can only read string-format c-chain genesis
265            c_chain_genesis,
266
267            message: genesis_file.message,
268        };
269        Ok(genesis)
270    }
271}
272
273/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/genesis#Allocation>
274#[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    /// "eth_addr" can be any value, not used in "avalanchego".
279    /// This field is only used for memos.
280    #[serde(rename = "ethAddr", skip_serializing_if = "Option::is_none")]
281    pub eth_addr: Option<String>,
282    /// Initially allocated amount for X-chain.
283    /// On the X-Chain, one AVAX is 10^9  units.
284    /// On the P-Chain, one AVAX is 10^9  units.
285    /// On the C-Chain, one AVAX is 10^18 units.
286    /// ref. <https://snowtrace.io/unitconverter>
287    #[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
293/// On the X-Chain, one AVAX is 10^9  units.
294/// On the P-Chain, one AVAX is 10^9  units.
295/// On the C-Chain, one AVAX is 10^18 units.
296/// 300,000,000 AVAX.
297/// ref. <https://snowtrace.io/unitconverter>
298pub const DEFAULT_INITIAL_AMOUNT_X_CHAIN: u64 = 300000000000000000;
299
300/// On the X-Chain, one AVAX is 10^9  units.
301/// On the P-Chain, one AVAX is 10^9  units.
302/// On the C-Chain, one AVAX is 10^18 units.
303/// 200,000,000 AVAX.
304/// ref. <https://snowtrace.io/unitconverter>
305pub 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/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/genesis#LockedAmount>
320#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
321pub struct LockedAmount {
322    /// P-chain amount to lock for the duration of "locktime"
323    /// in addition to the initial amount.
324    /// On the X-Chain, one AVAX is 10^9  units.
325    /// On the P-Chain, one AVAX is 10^9  units.
326    /// On the C-Chain, one AVAX is 10^18 units.
327    /// ref. <https://snowtrace.io/unitconverter>
328    #[serde(rename = "amount", skip_serializing_if = "Option::is_none")]
329    pub amount: Option<u64>,
330    /// Unix timestamp to unlock the "amount".
331    #[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        // NOTE: to place lock-time, use this:
338        // let now_unix = SystemTime::now()
339        //     .duration_since(SystemTime::UNIX_EPOCH)
340        //     .expect("unexpected None duration_since")
341        //     .as_secs();
342        // unlock_now.locktime = Some(now_unix);
343
344        Self {
345            amount: Some(DEFAULT_LOCKED_AMOUNT_P_CHAIN),
346            locktime: None, // empty to unlock immediately
347        }
348    }
349}
350
351/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/genesis#Staker>
352#[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}