Skip to main content

solana_genesis_config/
lib.rs

1//! The chain's genesis config.
2
3#![cfg_attr(feature = "frozen-abi", feature(min_specialization))]
4#![cfg_attr(docsrs, feature(doc_cfg))]
5#[cfg(feature = "frozen-abi")]
6use solana_frozen_abi_macro::{frozen_abi, AbiExample};
7#[cfg(feature = "serde")]
8use {
9    bincode::{deserialize, serialize},
10    chrono::{TimeZone, Utc},
11    memmap2::Mmap,
12    solana_hash::Hash,
13    solana_sha256_hasher::hash,
14    solana_shred_version::compute_shred_version,
15    std::{
16        fmt,
17        fs::{File, OpenOptions},
18        io::Write,
19        path::{Path, PathBuf},
20    },
21};
22use {
23    solana_account::{Account, AccountSharedData},
24    solana_clock::{UnixTimestamp, DEFAULT_TICKS_PER_SLOT},
25    solana_cluster_type::ClusterType,
26    solana_epoch_schedule::EpochSchedule,
27    solana_fee_calculator::FeeRateGovernor,
28    solana_inflation::Inflation,
29    solana_keypair::Keypair,
30    solana_poh_config::PohConfig,
31    solana_pubkey::Pubkey,
32    solana_rent::Rent,
33    solana_sdk_ids::system_program,
34    solana_signer::Signer,
35    solana_time_utils::years_as_slots,
36    std::{
37        collections::BTreeMap,
38        time::{SystemTime, UNIX_EPOCH},
39    },
40};
41
42pub const DEFAULT_GENESIS_FILE: &str = "genesis.bin";
43pub const DEFAULT_GENESIS_ARCHIVE: &str = "genesis.tar.bz2";
44pub const DEFAULT_GENESIS_DOWNLOAD_PATH: &str = "/genesis.tar.bz2";
45
46// deprecated default that is no longer used
47pub const UNUSED_DEFAULT: u64 = 1024;
48
49#[cfg_attr(
50    feature = "frozen-abi",
51    derive(AbiExample),
52    frozen_abi(digest = "5QrfiDhg9p5a9YRN8LrSUxEFj3M4PpBWRLruhefaQr2c")
53)]
54#[cfg_attr(
55    feature = "serde",
56    derive(serde_derive::Deserialize, serde_derive::Serialize)
57)]
58#[derive(Clone, Debug, PartialEq)]
59pub struct GenesisConfig {
60    /// when the network (bootstrap validator) was started relative to the UNIX Epoch
61    pub creation_time: UnixTimestamp,
62    /// initial accounts
63    pub accounts: BTreeMap<Pubkey, Account>,
64    /// built-in programs
65    pub native_instruction_processors: Vec<(String, Pubkey)>,
66    /// accounts for network rewards, these do not count towards capitalization
67    pub rewards_pools: BTreeMap<Pubkey, Account>,
68    pub ticks_per_slot: u64,
69    pub unused: u64,
70    /// network speed configuration
71    pub poh_config: PohConfig,
72    /// this field exists only to ensure that the binary layout of GenesisConfig remains compatible
73    /// with the Solana v0.23 release line
74    pub __backwards_compat_with_v0_23: u64,
75    /// transaction fee config
76    pub fee_rate_governor: FeeRateGovernor,
77    /// rent config
78    pub rent: Rent,
79    /// inflation config
80    pub inflation: Inflation,
81    /// how slots map to epochs
82    pub epoch_schedule: EpochSchedule,
83    /// network runlevel
84    pub cluster_type: ClusterType,
85}
86
87// useful for basic tests
88pub fn create_genesis_config(lamports: u64) -> (GenesisConfig, Keypair) {
89    let faucet_keypair = Keypair::new();
90    (
91        GenesisConfig::new(
92            &[(
93                faucet_keypair.pubkey(),
94                AccountSharedData::new(lamports, 0, &system_program::id()),
95            )],
96            &[],
97        ),
98        faucet_keypair,
99    )
100}
101
102impl Default for GenesisConfig {
103    fn default() -> Self {
104        Self {
105            creation_time: SystemTime::now()
106                .duration_since(UNIX_EPOCH)
107                .unwrap()
108                .as_secs() as UnixTimestamp,
109            accounts: BTreeMap::default(),
110            native_instruction_processors: Vec::default(),
111            rewards_pools: BTreeMap::default(),
112            ticks_per_slot: DEFAULT_TICKS_PER_SLOT,
113            unused: UNUSED_DEFAULT,
114            poh_config: PohConfig::default(),
115            inflation: Inflation::default(),
116            __backwards_compat_with_v0_23: 0,
117            fee_rate_governor: FeeRateGovernor::default(),
118            rent: Rent::default(),
119            epoch_schedule: EpochSchedule::default(),
120            cluster_type: ClusterType::Development,
121        }
122    }
123}
124
125impl GenesisConfig {
126    pub fn new(
127        accounts: &[(Pubkey, AccountSharedData)],
128        native_instruction_processors: &[(String, Pubkey)],
129    ) -> Self {
130        Self {
131            accounts: accounts
132                .iter()
133                .cloned()
134                .map(|(key, account)| (key, Account::from(account)))
135                .collect::<BTreeMap<Pubkey, Account>>(),
136            native_instruction_processors: native_instruction_processors.to_vec(),
137            ..GenesisConfig::default()
138        }
139    }
140
141    #[cfg(feature = "serde")]
142    pub fn hash(&self) -> Hash {
143        let serialized = serialize(&self).unwrap();
144        hash(&serialized)
145    }
146
147    #[cfg(feature = "serde")]
148    fn genesis_filename(ledger_path: &Path) -> PathBuf {
149        Path::new(ledger_path).join(DEFAULT_GENESIS_FILE)
150    }
151
152    #[cfg(feature = "serde")]
153    pub fn load(ledger_path: &Path) -> Result<Self, std::io::Error> {
154        let filename = Self::genesis_filename(ledger_path);
155        let file = OpenOptions::new()
156            .read(true)
157            .open(&filename)
158            .map_err(|err| {
159                std::io::Error::other(format!("Unable to open {filename:?}: {err:?}"))
160            })?;
161
162        //UNSAFE: Required to create a Mmap
163        let mem = unsafe { Mmap::map(&file) }
164            .map_err(|err| std::io::Error::other(format!("Unable to map {filename:?}: {err:?}")))?;
165
166        let genesis_config = deserialize(&mem).map_err(|err| {
167            std::io::Error::other(format!("Unable to deserialize {filename:?}: {err:?}"))
168        })?;
169        Ok(genesis_config)
170    }
171
172    #[cfg(feature = "serde")]
173    pub fn write(&self, ledger_path: &Path) -> Result<(), std::io::Error> {
174        let serialized = serialize(&self)
175            .map_err(|err| std::io::Error::other(format!("Unable to serialize: {err:?}")))?;
176
177        std::fs::create_dir_all(ledger_path)?;
178
179        let mut file = File::create(Self::genesis_filename(ledger_path))?;
180        file.write_all(&serialized)
181    }
182
183    pub fn add_account(&mut self, pubkey: Pubkey, account: AccountSharedData) {
184        self.accounts.insert(pubkey, Account::from(account));
185    }
186
187    pub fn add_native_instruction_processor(&mut self, name: String, program_id: Pubkey) {
188        self.native_instruction_processors.push((name, program_id));
189    }
190
191    pub fn hashes_per_tick(&self) -> Option<u64> {
192        self.poh_config.hashes_per_tick
193    }
194
195    pub fn ticks_per_slot(&self) -> u64 {
196        self.ticks_per_slot
197    }
198
199    pub fn ns_per_slot(&self) -> u128 {
200        self.poh_config
201            .target_tick_duration
202            .as_nanos()
203            .saturating_mul(self.ticks_per_slot() as u128)
204    }
205
206    pub fn slots_per_year(&self) -> f64 {
207        years_as_slots(
208            1.0,
209            &self.poh_config.target_tick_duration,
210            self.ticks_per_slot(),
211        )
212    }
213}
214
215#[cfg(feature = "serde")]
216impl fmt::Display for GenesisConfig {
217    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
218        write!(
219            f,
220            "\
221             Creation time: {}\n\
222             Cluster type: {:?}\n\
223             Genesis hash: {}\n\
224             Shred version: {}\n\
225             Ticks per slot: {:?}\n\
226             Hashes per tick: {:?}\n\
227             Target tick duration: {:?}\n\
228             Slots per epoch: {}\n\
229             Warmup epochs: {}abled\n\
230             Slots per year: {}\n\
231             {:?}\n\
232             {:?}\n\
233             {:?}\n\
234             Capitalization: {} lamports in {} accounts\n\
235             Native instruction processors: {:#?}\n\
236             Rewards pool: {:#?}\n\
237             ",
238            Utc.timestamp_opt(self.creation_time, 0)
239                .unwrap()
240                .to_rfc3339(),
241            self.cluster_type,
242            self.hash(),
243            compute_shred_version(&self.hash(), None),
244            self.ticks_per_slot,
245            self.poh_config.hashes_per_tick,
246            self.poh_config.target_tick_duration,
247            self.epoch_schedule.slots_per_epoch,
248            if self.epoch_schedule.warmup {
249                "en"
250            } else {
251                "dis"
252            },
253            self.slots_per_year(),
254            self.inflation,
255            self.rent,
256            self.fee_rate_governor,
257            self.accounts
258                .iter()
259                .map(|(pubkey, account)| {
260                    assert!(account.lamports > 0, "{:?}", (pubkey, account));
261                    account.lamports
262                })
263                .sum::<u64>(),
264            self.accounts.len(),
265            self.native_instruction_processors,
266            self.rewards_pools,
267        )
268    }
269}
270
271#[cfg(all(feature = "serde", test))]
272mod tests {
273    use {
274        super::*,
275        solana_signer::Signer,
276        std::{path::PathBuf, str::FromStr},
277    };
278
279    fn make_tmp_path(name: &str) -> PathBuf {
280        let out_dir = std::env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string());
281        let keypair = Keypair::new();
282
283        let path = [
284            out_dir,
285            "tmp".to_string(),
286            format!("{}-{}", name, keypair.pubkey()),
287        ]
288        .iter()
289        .collect();
290
291        // whack any possible collision
292        let _ignored = std::fs::remove_dir_all(&path);
293        // whack any possible collision
294        let _ignored = std::fs::remove_file(&path);
295
296        path
297    }
298
299    #[test]
300    fn cluster_compatibility() {
301        for cluster in ["devnet", "testnet", "mainnet-beta"] {
302            let ledger_path = PathBuf::from(format!("tests/fixtures/{cluster}"));
303            let genesis_config = GenesisConfig::load(&ledger_path).unwrap();
304            let cluster_type = ClusterType::from_str(cluster).unwrap();
305            let expected_hash = cluster_type.get_genesis_hash().unwrap();
306
307            assert_eq!(
308                expected_hash,
309                genesis_config.hash(),
310                "{cluster} genesis hash mismatch",
311            );
312        }
313    }
314
315    #[test]
316    fn test_genesis_config() {
317        let faucet_keypair = Keypair::new();
318        let mut config = GenesisConfig::default();
319        config.add_account(
320            faucet_keypair.pubkey(),
321            AccountSharedData::new(10_000, 0, &Pubkey::default()),
322        );
323        config.add_account(
324            solana_pubkey::new_rand(),
325            AccountSharedData::new(1, 0, &Pubkey::default()),
326        );
327        config.add_native_instruction_processor("hi".to_string(), solana_pubkey::new_rand());
328
329        assert_eq!(config.accounts.len(), 2);
330        assert!(config
331            .accounts
332            .iter()
333            .any(|(pubkey, account)| *pubkey == faucet_keypair.pubkey()
334                && account.lamports == 10_000));
335
336        let path = &make_tmp_path("genesis_config");
337        config.write(path).expect("write");
338        let loaded_config = GenesisConfig::load(path).expect("load");
339        assert_eq!(config.hash(), loaded_config.hash());
340        let _ignored = std::fs::remove_file(path);
341    }
342}