fuel_core_chain_config/config/
chain.rs

1use fuel_core_storage::MerkleRoot;
2use fuel_core_types::{
3    blockchain::header::StateTransitionBytecodeVersion,
4    fuel_crypto::Hasher,
5    fuel_tx::ConsensusParameters,
6    fuel_types::{
7        fmt_truncated_hex,
8        AssetId,
9    },
10};
11use serde::{
12    Deserialize,
13    Serialize,
14};
15#[cfg(feature = "std")]
16use std::fs::File;
17#[cfg(feature = "std")]
18use std::path::Path;
19
20use crate::{
21    genesis::GenesisCommitment,
22    ConsensusConfig,
23};
24
25#[cfg(feature = "std")]
26use crate::SnapshotMetadata;
27
28pub const LOCAL_TESTNET: &str = "local_testnet";
29pub const BYTECODE_NAME: &str = "state_transition_bytecode.wasm";
30
31#[derive(Clone, derivative::Derivative, Deserialize, Serialize, Eq, PartialEq)]
32#[derivative(Debug)]
33pub struct ChainConfig {
34    pub chain_name: String,
35    pub consensus_parameters: ConsensusParameters,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    #[serde(default)]
38    pub genesis_state_transition_version: Option<StateTransitionBytecodeVersion>,
39    /// Note: The state transition bytecode is stored in a separate file
40    /// under the `BYTECODE_NAME` name in serialization form.
41    #[serde(skip)]
42    #[derivative(Debug(format_with = "fmt_truncated_hex::<16>"))]
43    pub state_transition_bytecode: Vec<u8>,
44    pub consensus: ConsensusConfig,
45}
46
47#[cfg(feature = "test-helpers")]
48impl Default for ChainConfig {
49    fn default() -> Self {
50        Self {
51            chain_name: "local".into(),
52            consensus_parameters: ConsensusParameters::default(),
53            genesis_state_transition_version: Some(
54                fuel_core_types::blockchain::header::LATEST_STATE_TRANSITION_VERSION,
55            ),
56            // Note: It is invalid bytecode.
57            state_transition_bytecode: vec![],
58            consensus: ConsensusConfig::default_poa(),
59        }
60    }
61}
62
63impl ChainConfig {
64    pub const BASE_ASSET: AssetId = AssetId::zeroed();
65
66    #[cfg(feature = "std")]
67    pub fn load(path: impl AsRef<Path>) -> anyhow::Result<Self> {
68        use std::io::Read;
69        let path = path.as_ref();
70        let mut json = String::new();
71        std::fs::File::open(path)?.read_to_string(&mut json)?;
72        let mut chain_config: ChainConfig =
73            serde_json::from_str(json.as_str()).map_err(|e| {
74                anyhow::Error::new(e).context(format!(
75                    "an error occurred while loading the chain state file: {:?}",
76                    path.to_str()
77                ))
78            })?;
79
80        let bytecode_path = path.with_file_name(BYTECODE_NAME);
81
82        let bytecode = if bytecode_path.exists() {
83            std::fs::read(bytecode_path).map_err(|e| {
84                anyhow::Error::new(e).context(format!(
85                    "an error occurred while loading the state transition bytecode: {:?}",
86                    path.to_str()
87                ))
88            })?
89        } else {
90            Vec::new()
91        };
92        chain_config.state_transition_bytecode = bytecode;
93
94        Ok(chain_config)
95    }
96
97    #[cfg(feature = "std")]
98    pub fn from_snapshot_metadata(
99        snapshot_metadata: &SnapshotMetadata,
100    ) -> anyhow::Result<Self> {
101        Self::load(&snapshot_metadata.chain_config)
102    }
103
104    #[cfg(feature = "std")]
105    pub fn write(&self, path: impl AsRef<Path>) -> anyhow::Result<()> {
106        use anyhow::Context;
107
108        let bytecode_path = path.as_ref().with_file_name(BYTECODE_NAME);
109        let chain_config_file = File::create(path)?;
110
111        serde_json::to_writer_pretty(chain_config_file, self)
112            .context("failed to dump chain parameters snapshot to JSON")?;
113        std::fs::write(bytecode_path, &self.state_transition_bytecode)
114            .context("failed to write state transition bytecode")?;
115
116        Ok(())
117    }
118
119    #[cfg(feature = "test-helpers")]
120    pub fn local_testnet() -> Self {
121        Self {
122            chain_name: LOCAL_TESTNET.to_string(),
123            ..Default::default()
124        }
125    }
126
127    #[cfg(feature = "test-helpers")]
128    pub fn local_testnet_with_consensus_parameters(cp: &ConsensusParameters) -> Self {
129        Self {
130            chain_name: LOCAL_TESTNET.to_string(),
131            consensus_parameters: cp.clone(),
132            ..Default::default()
133        }
134    }
135}
136
137impl GenesisCommitment for ChainConfig {
138    fn root(&self) -> anyhow::Result<MerkleRoot> {
139        let chain_config_bytes =
140            postcard::to_allocvec(&self).map_err(anyhow::Error::msg)?;
141        let config_hash = *Hasher::default()
142            .chain(chain_config_bytes.as_slice())
143            .chain(self.state_transition_bytecode.as_slice())
144            .finalize();
145
146        Ok(config_hash)
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    #[cfg(feature = "std")]
153    use std::env::temp_dir;
154
155    use super::ChainConfig;
156
157    #[cfg(feature = "std")]
158    #[test]
159    fn can_roundtrip_write_and_read() {
160        let tmp_dir = temp_dir();
161        let file = tmp_dir.join("config.json");
162
163        let disk_config = ChainConfig::local_testnet();
164        disk_config.write(&file).unwrap();
165
166        let load_config = ChainConfig::load(&file).unwrap();
167
168        assert_eq!(disk_config, load_config);
169    }
170
171    #[test]
172    fn snapshot_local_testnet_config() {
173        let config = ChainConfig::local_testnet();
174        let json = serde_json::to_string_pretty(&config).unwrap();
175        insta::assert_snapshot!(json);
176    }
177}