amadeus_node/consensus/
mod.rs

1#![allow(clippy::module_inception)]
2pub mod consensus;
3pub mod doms;
4pub mod fabric;
5pub mod genesis;
6
7// Re-export DST constants from amadeus_utils
8pub use amadeus_utils::constants::{
9    DST, DST_ANR, DST_ANR_CHALLENGE, DST_ATT, DST_ENTRY, DST_MOTION, DST_NODE, DST_POP, DST_TX, DST_VRF,
10};
11
12use crate::utils::misc::TermExt;
13use crate::utils::rocksdb::RocksDb;
14use amadeus_utils::constants::CF_SYSCONF;
15use eetf::Term;
16
17/// Chain epoch accessor (Elixir: Consensus.chain_epoch/0)
18/// Returns current epoch calculated as height / 100_000
19pub fn chain_epoch(db: &RocksDb) -> u32 {
20    chain_height(db) / 100_000
21}
22
23/// Chain height accessor - gets current blockchain height
24pub fn chain_height(db: &RocksDb) -> u32 {
25    match db.get(CF_SYSCONF, b"temporal_height") {
26        Ok(Some(bytes)) => {
27            // Elixir stores as ETF term with `term: true`
28            match Term::decode(&bytes[..]) {
29                Ok(term) => TermExt::get_integer(&term).unwrap_or(0) as u32,
30                Err(_) => 0, // fallback if deserialization fails
31            }
32        }
33        _ => 0, // fallback if key not found
34    }
35}
36
37#[cfg(test)]
38mod tests {
39    use crate::consensus::consensus::apply_entry;
40    use crate::consensus::fabric::Fabric;
41    use crate::utils::Hash;
42    use eetf::Term;
43    use std::path::Path;
44
45    #[tokio::test]
46    #[ignore = "requires migrated database snapshot with new CF schema"]
47    async fn test_apply_entry_34076357() -> Result<(), Box<dyn std::error::Error>> {
48        let hash = bs58::decode("DEYRMxK3rCgVvwFagmpJQecbreiLUeYjRxrVfs6yKiJ5").into_vec()?;
49        test_apply_entry_at_height(34076356, hash.try_into().map_err(|_| "invalid hash")?).await
50    }
51
52    #[tokio::test]
53    #[ignore = "requires migrated database snapshot with new CF schema"]
54    async fn test_apply_entry_34076383() -> Result<(), Box<dyn std::error::Error>> {
55        let hash = bs58::decode("53NtszVMj5nBA7PnaDsLtiSZAX6T6LvmH74BngSVtp6C").into_vec()?;
56        test_apply_entry_at_height(34076382, hash.try_into().map_err(|_| "invalid hash")?).await
57    }
58
59    #[tokio::test]
60    #[ignore = "requires migrated database snapshot with new CF schema"]
61    async fn test_apply_entry_34076433() -> Result<(), Box<dyn std::error::Error>> {
62        let hash = bs58::decode("12mVLz4waDiBb9qqqnD5KLJMxRvAMaDz6W1pidXA1cm6").into_vec()?;
63        test_apply_entry_at_height(34076432, hash.try_into().map_err(|_| "invalid hash")?).await
64    }
65
66    #[tokio::test]
67    #[ignore = "requires migrated database snapshot with new CF schema"]
68    async fn test_apply_entry_34099999() -> Result<(), Box<dyn std::error::Error>> {
69        let hash = bs58::decode("fR28rtEYFfm6iwWDrJXvQjEBJNT1NZNak52NTwFuiU3").into_vec()?;
70        test_apply_entry_at_height(34099998, hash.try_into().map_err(|_| "invalid hash")?).await
71    }
72
73    async fn test_apply_entry_at_height(
74        height: u32,
75        expected_muts_hash: Hash,
76    ) -> Result<(), Box<dyn std::error::Error>> {
77        let db_path = format!("../assets/rocksdb/{}", height);
78        assert!(Path::new(&db_path).exists(), "Test database snapshot not found: {}", db_path);
79
80        // copy db to temp
81        let temp = format!("/tmp/test-rocksdb-{}-{}", height, std::process::id());
82        let temp_db = format!("{}/db/fabric", temp);
83        if Path::new(&temp).exists() {
84            std::fs::remove_dir_all(&temp)?;
85        }
86        copy_dir(&format!("{}/fabric", db_path), &temp_db)?;
87
88        // open fabric
89        let fabric = Fabric::new(&temp).await?;
90
91        // get entry at height+1 from db
92        let next_height = height + 1;
93        let entries = fabric.entries_by_height(next_height as u64)?;
94        let entry = crate::consensus::doms::entry::Entry::from_vecpak_bin(&entries[0])?;
95
96        // read and print expected logs from next_logs file
97        let expected_logs_path = format!("{}/next_logs", db_path);
98        if Path::new(&expected_logs_path).exists() {
99            let expected_logs_bin = std::fs::read(&expected_logs_path)?;
100            println!("\n=== Expected logs from next_logs file ===");
101            match decode_logs(&expected_logs_bin) {
102                Ok(logs) => {
103                    for (i, (error, log_list)) in logs.iter().enumerate() {
104                        println!("Transaction {}: error={:?}, logs={:?}", i, error, log_list);
105                    }
106                }
107                Err(e) => println!("Failed to decode expected logs: {}", e),
108            }
109        } else {
110            println!("\n=== No next_logs file found ===");
111        }
112
113        // apply entry
114        let config = create_test_config();
115        apply_entry(&fabric, &config, &entry)?;
116
117        // get results
118        let muts = crate::consensus::consensus::chain_muts(&fabric, &entry.hash).ok_or("no muts")?;
119        let muts_rev = crate::consensus::consensus::chain_muts_rev(&fabric, &entry.hash).ok_or("no muts_rev")?;
120
121        // decode expected from elixir
122        let exp_muts = decode_muts(&std::fs::read(format!("{}/next_muts", db_path))?)?;
123        let exp_muts_rev = decode_muts(&std::fs::read(format!("{}/next_muts_rev", db_path))?)?;
124
125        // compare mutations
126        assert_eq!(muts.len(), exp_muts.len(), "muts count");
127        assert_eq!(muts_rev.len(), exp_muts_rev.len(), "muts_rev count");
128
129        for (i, (r, e)) in muts.iter().zip(exp_muts.iter()).enumerate() {
130            assert_eq!(r, e, "muts[{}] mismatch", i);
131        }
132
133        // verify mutations hash
134        let my_att = fabric.my_attestation_by_entryhash(&*entry.hash)?.ok_or("no attestation")?;
135        assert_eq!(my_att.mutations_hash, expected_muts_hash, "mutations hash mismatch");
136
137        std::fs::remove_dir_all(&temp).ok();
138        Ok(())
139    }
140
141    fn decode_logs(bin: &[u8]) -> Result<Vec<(String, Vec<String>)>, Box<dyn std::error::Error>> {
142        use crate::utils::misc::TermExt;
143        let term = Term::decode(bin)?;
144        let outer_list = match &term {
145            Term::List(l) => l,
146            _ => return Err("not list".into()),
147        };
148
149        outer_list
150            .elements
151            .iter()
152            .map(|e| {
153                let m = match e {
154                    Term::Map(m) => &m.map,
155                    _ => return Err("not map".into()),
156                };
157
158                // get error field
159                let error = match m.get(&Term::Atom(eetf::Atom::from("error"))) {
160                    Some(Term::Atom(a)) => a.name.as_str().to_string(),
161                    _ => return Err("no error field".into()),
162                };
163
164                // get logs field (list of binaries)
165                let logs = match m.get(&Term::Atom(eetf::Atom::from("logs"))) {
166                    Some(Term::List(log_list)) => log_list
167                        .elements
168                        .iter()
169                        .filter_map(|log_term| {
170                            log_term.get_binary().map(|bytes| String::from_utf8_lossy(&bytes).to_string())
171                        })
172                        .collect(),
173                    _ => vec![],
174                };
175
176                Ok((error, logs))
177            })
178            .collect()
179    }
180
181    fn decode_muts(
182        bin: &[u8],
183    ) -> Result<Vec<amadeus_runtime::consensus::consensus_muts::Mutation>, Box<dyn std::error::Error>> {
184        use crate::utils::misc::TermExt;
185        let term = Term::decode(bin)?;
186        let list = match &term {
187            Term::List(l) => l,
188            _ => return Err("not list".into()),
189        };
190        list.elements
191            .iter()
192            .map(|e| {
193                let m = match e {
194                    Term::Map(m) => &m.map,
195                    _ => return Err("not map".into()),
196                };
197                let op = match m.get(&Term::Atom(eetf::Atom::from("op"))) {
198                    Some(Term::Atom(a)) => a,
199                    _ => return Err("no op".into()),
200                };
201                let key =
202                    m.get(&Term::Atom(eetf::Atom::from("key"))).and_then(|t| t.get_binary()).ok_or("no key")?.to_vec();
203
204                match op.name.as_str() {
205                    "put" => {
206                        let value = m
207                            .get(&Term::Atom(eetf::Atom::from("value")))
208                            .and_then(|t| t.get_binary())
209                            .ok_or("no val")?
210                            .to_vec();
211                        Ok(amadeus_runtime::consensus::consensus_muts::Mutation::Put { op: vec![], key, value })
212                    }
213                    "delete" => Ok(amadeus_runtime::consensus::consensus_muts::Mutation::Delete { op: vec![], key }),
214                    "set_bit" => {
215                        let value = m
216                            .get(&Term::Atom(eetf::Atom::from("value")))
217                            .and_then(|t| if let Term::FixInteger(i) = t { Some(i.value) } else { None })
218                            .ok_or("no bit")? as u64;
219                        let bloomsize = m
220                            .get(&Term::Atom(eetf::Atom::from("bloomsize")))
221                            .and_then(|t| if let Term::FixInteger(i) = t { Some(i.value) } else { None })
222                            .ok_or("no size")? as u64;
223                        Ok(amadeus_runtime::consensus::consensus_muts::Mutation::SetBit {
224                            op: vec![],
225                            key,
226                            value,
227                            bloomsize,
228                        })
229                    }
230                    "clear_bit" => {
231                        let value = m
232                            .get(&Term::Atom(eetf::Atom::from("value")))
233                            .and_then(|t| if let Term::FixInteger(i) = t { Some(i.value) } else { None })
234                            .ok_or("no bit")? as u64;
235                        Ok(amadeus_runtime::consensus::consensus_muts::Mutation::ClearBit { op: vec![], key, value })
236                    }
237                    _ => Err("unknown op".into()),
238                }
239            })
240            .collect()
241    }
242
243    fn copy_dir(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
244        std::fs::create_dir_all(&dst)?;
245        for e in std::fs::read_dir(src)? {
246            let e = e?;
247            if e.file_type()?.is_dir() {
248                copy_dir(e.path(), dst.as_ref().join(e.file_name()))?;
249            } else {
250                std::fs::copy(e.path(), dst.as_ref().join(e.file_name()))?;
251            }
252        }
253        Ok(())
254    }
255
256    fn create_test_config() -> crate::config::Config {
257        let sk = crate::config::gen_sk();
258        let pk = crate::config::get_pk(&sk);
259        let pop = crate::utils::bls12_381::sign(&sk, pk.as_ref(), amadeus_utils::constants::DST_POP)
260            .map(|sig| sig.to_vec())
261            .unwrap_or_else(|_| vec![0u8; 96]);
262        crate::config::Config {
263            work_folder: "/tmp/test".to_string(),
264            version: crate::config::VERSION,
265            offline: false,
266            http_ipv4: std::net::Ipv4Addr::LOCALHOST,
267            http_port: 80,
268            udp_ipv4: std::net::Ipv4Addr::LOCALHOST,
269            udp_port: 36969,
270            public_ipv4: None,
271            seed_ips: vec![],
272            seed_anrs: vec![],
273            other_nodes: vec![],
274            trust_factor: 0.8,
275            max_peers: 300,
276            trainer_sk: sk,
277            trainer_pk: pk,
278            trainer_pk_b58: "test".to_string(),
279            trainer_pop: pop,
280            archival_node: false,
281            autoupdate: false,
282            computor_type: None,
283            snapshot_height: 0,
284            anr: None,
285            anr_name: None,
286            anr_desc: None,
287        }
288    }
289}