saito_core/core/util/
balance_snapshot.rs

1use std::fmt::{Display, Formatter, Write};
2use std::io::{BufRead, BufReader};
3
4use log::{debug, info};
5
6use crate::core::consensus::slip::{Slip, SlipType};
7use crate::core::defs::{
8    BlockId, PrintForLog, SaitoHash, SaitoPublicKey, Timestamp, UTXO_KEY_LENGTH,
9};
10
11pub type BalanceFileRowType = (String, String, String, String);
12
13pub struct BalanceSnapshot {
14    pub latest_block_id: BlockId,
15    pub latest_block_hash: SaitoHash,
16    pub timestamp: Timestamp,
17    pub slips: Vec<Slip>,
18}
19
20impl BalanceSnapshot {
21    /// Converts the internal data into text format
22    ///
23    /// Following is the file format
24    ///
25    /// | Public Key | Block Id | Transaction Id | Slip Id | Amount |
26    ///
27    /// Following is the file name format
28    ///
29    /// <timestamp>-<latest_block_id>-<latest_block_hash>.snap
30    ///
31    /// # Parameters
32    /// None.
33    ///
34    /// # Returns
35    /// This function returns a tuple containing:
36    ///
37    /// - A `String`: File name
38    /// - A `Vec<String>`: Rows of the file
39    ///
40    pub fn get_data(&self) -> (String, Vec<String>) {
41        let file_name: String = self.get_file_name();
42        let entries: Vec<String> = self.get_rows();
43
44        (file_name, entries)
45    }
46
47    pub fn get_file_name(&self) -> String {
48        self.timestamp.to_string()
49            + "-"
50            + self.latest_block_id.to_string().as_str()
51            + "-"
52            + self.latest_block_hash.to_hex().as_str()
53            + ".snap"
54    }
55    pub fn get_rows(&self) -> Vec<String> {
56        self.slips
57            .iter()
58            .map(|slip| {
59                let key = slip.public_key.to_base58();
60                let entry = format!(
61                    "{} {:?} {:?} {:?} {:?}",
62                    key, slip.block_id, slip.tx_ordinal, slip.slip_index, slip.amount
63                );
64                entry
65            })
66            .collect()
67    }
68
69    pub fn new(file_name: String, rows: Vec<String>) -> Result<BalanceSnapshot, String> {
70        debug!(
71            "creating new balance snapshot from file : {:?} with {:?} rows",
72            file_name,
73            rows.len()
74        );
75        let mut tokens: Vec<&str> = file_name.split('.').collect();
76        let file_name = tokens.remove(0);
77        let tokens: Vec<&str> = file_name.split('-').collect();
78        if tokens.len() != 3 {
79            return Err(format!(
80                "file name : {:?} is invalid for balance snapshot file",
81                file_name
82            ));
83        }
84        let timestamp = tokens.get(0).unwrap();
85        let block_id = tokens.get(1).unwrap();
86        let block_hash = tokens.get(2).unwrap();
87
88        let timestamp: Timestamp = timestamp
89            .parse()
90            .map_err(|err| format!("failed parsing timestamp : {:?}. {:?}", timestamp, err))?;
91        let block_id: u64 = block_id
92            .parse()
93            .map_err(|err| format!("failed parsing block id : {:?}. {:?}", block_id, err))?;
94        let block_hash = SaitoHash::from_hex(block_hash)
95            .map_err(|err| format!("failed parsing block hash : {:?}. {:?}", block_hash, err))?;
96
97        let mut snapshot = BalanceSnapshot {
98            latest_block_id: block_id,
99            latest_block_hash: block_hash,
100            timestamp,
101            slips: vec![],
102        };
103
104        rows.iter().try_for_each(|row| {
105            let cols: Vec<&str> = row.split(' ').collect();
106            if cols.len() != 5 {
107                return Err(format!(
108                    "row is invalid. number of columns is {:?}. but should be 5",
109                    cols.len()
110                ));
111            }
112            let key = cols.get(0).ok_or("cannot find key in row".to_string())?;
113            let block_id = cols
114                .get(1)
115                .ok_or("cannot find block id in row".to_string())?;
116            let tx_id = cols.get(2).ok_or("cannot find tx id in row".to_string())?;
117            let slip_id = cols
118                .get(3)
119                .ok_or("cannot find slip id in row".to_string())?;
120            let amount = cols.get(4).ok_or("cannot find amount in row".to_string())?;
121
122            let key: SaitoPublicKey = SaitoPublicKey::from_base58(key)
123                .or(Err(format!("failed parsing key : {:?}", key)))?;
124            let block_id = block_id
125                .parse()
126                .or(Err(format!("failed parsing block id : {:?}", block_id)))?;
127            let tx_id = tx_id
128                .parse()
129                .or(Err(format!("failed parsing tx id : {:?}", tx_id)))?;
130            let slip_id = slip_id
131                .parse()
132                .or(Err(format!("failed parsing slip id : {:?}", slip_id)))?;
133            let amount = amount
134                .parse()
135                .or(Err(format!("failed parsing amount : {:?}", amount)))?;
136
137            let mut slip = Slip {
138                public_key: key,
139                amount,
140                slip_index: slip_id,
141                block_id,
142                tx_ordinal: tx_id,
143                slip_type: SlipType::Normal,
144                utxoset_key: [0; UTXO_KEY_LENGTH],
145                is_utxoset_key_set: false,
146            };
147            slip.generate_utxoset_key();
148            snapshot.slips.push(slip);
149
150            Ok(())
151        })?;
152
153        Ok(snapshot)
154    }
155}
156
157impl Display for BalanceSnapshot {
158    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
159        let (file_name, rows) = self.get_data();
160        writeln!(f, "{}", file_name).unwrap();
161        rows.iter().for_each(|row| {
162            writeln!(f, "{}", row).unwrap();
163        });
164
165        std::fmt::Result::Ok(())
166    }
167}
168
169impl TryFrom<String> for BalanceSnapshot {
170    type Error = String;
171
172    fn try_from(value: String) -> Result<Self, Self::Error> {
173        let mut reader = BufReader::new(value.as_bytes());
174
175        let mut file_name = "".to_string();
176        reader
177            .read_line(&mut file_name)
178            .map_err(|_err| "failed reading file name from balance snapshot")?;
179        file_name.pop();
180
181        let mut rows = vec![];
182        loop {
183            let mut row = "".to_string();
184            if reader.read_line(&mut row).is_ok() {
185                info!("row = {}", row);
186                if row.is_empty() {
187                    break;
188                }
189                // removing the new line
190                row.pop();
191                rows.push(row);
192            } else {
193                break;
194            }
195        }
196
197        BalanceSnapshot::new(file_name, rows)
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use log::info;
204
205    use crate::core::consensus::slip::{Slip, SlipType};
206    use crate::core::defs::{PrintForLog, UTXO_KEY_LENGTH};
207    use crate::core::util::balance_snapshot::BalanceSnapshot;
208
209    #[test]
210    fn load_save_test() {
211        let mut snapshot = BalanceSnapshot {
212            latest_block_id: 200,
213            latest_block_hash: [1; 32],
214            timestamp: 10000,
215            slips: vec![],
216        };
217        snapshot.slips.push(Slip {
218            public_key: [1; 33],
219            amount: 10,
220            slip_index: 1,
221            block_id: 1,
222            tx_ordinal: 1,
223            slip_type: SlipType::Normal,
224            utxoset_key: [0; UTXO_KEY_LENGTH],
225            is_utxoset_key_set: false,
226        });
227        snapshot.slips.push(Slip {
228            public_key: [2; 33],
229            amount: 20,
230            slip_index: 2,
231            block_id: 2,
232            tx_ordinal: 2,
233            slip_type: SlipType::Normal,
234            utxoset_key: [0; UTXO_KEY_LENGTH],
235            is_utxoset_key_set: false,
236        });
237        snapshot.slips.push(Slip {
238            public_key: [3; 33],
239            amount: 30,
240            slip_index: 3,
241            block_id: 3,
242            tx_ordinal: 3,
243            slip_type: SlipType::Normal,
244            utxoset_key: [0; UTXO_KEY_LENGTH],
245            is_utxoset_key_set: false,
246        });
247        let (file_name, rows) = snapshot.get_data();
248        assert!(!file_name.is_empty());
249        let expected_file_name = snapshot.timestamp.to_string()
250            + "-"
251            + snapshot.latest_block_id.to_string().as_str()
252            + "-"
253            + snapshot.latest_block_hash.to_hex().as_str()
254            + ".snap";
255        assert_eq!(file_name, expected_file_name);
256
257        assert_eq!(rows.len(), 3);
258        for (index, row) in rows.iter().enumerate() {
259            let slip = snapshot.slips.get(index);
260            assert!(slip.is_some());
261            let slip = slip.unwrap();
262            let key = slip.public_key.to_base58();
263            info!("key = {:?}", key);
264            let expected_str = format!(
265                "{} {:?} {:?} {:?} {:?}",
266                key, slip.block_id, slip.tx_ordinal, slip.slip_index, slip.amount
267            );
268            info!("{:?} row = {:?}", index, expected_str);
269
270            assert_eq!(row.as_str(), expected_str.as_str());
271        }
272        let file_name = file_name
273            .split('.')
274            .collect::<Vec<&str>>()
275            .first()
276            .unwrap()
277            .to_string();
278        assert!(!file_name.is_empty());
279        let snapshot2 = BalanceSnapshot::new(file_name, rows);
280        assert!(snapshot2.is_ok(), "error : {:?}", snapshot2.err().unwrap());
281        let snapshot2 = snapshot2.unwrap();
282        assert_eq!(snapshot.timestamp, snapshot2.timestamp);
283        assert_eq!(snapshot.latest_block_id, snapshot2.latest_block_id);
284        assert_eq!(snapshot.latest_block_hash, snapshot2.latest_block_hash);
285
286        for (index, slip) in snapshot.slips.iter().enumerate() {
287            let slip2 = snapshot2.slips.get(index).unwrap();
288            assert_eq!(slip.public_key, slip2.public_key);
289            assert_eq!(slip.block_id, slip2.block_id);
290            assert_eq!(slip.tx_ordinal, slip2.tx_ordinal);
291            assert_eq!(slip.amount, slip2.amount);
292        }
293    }
294
295    #[test]
296    fn to_string_test() {
297        // pretty_env_logger::init();
298        let mut snapshot = BalanceSnapshot {
299            latest_block_id: 200,
300            latest_block_hash: [1; 32],
301            timestamp: 10000,
302            slips: vec![],
303        };
304        snapshot.slips.push(Slip {
305            public_key: [1; 33],
306            amount: 10,
307            slip_index: 1,
308            block_id: 1,
309            tx_ordinal: 1,
310            slip_type: SlipType::Normal,
311            utxoset_key: [0; UTXO_KEY_LENGTH],
312            is_utxoset_key_set: false,
313        });
314        snapshot.slips.push(Slip {
315            public_key: [2; 33],
316            amount: 20,
317            slip_index: 2,
318            block_id: 2,
319            tx_ordinal: 2,
320            slip_type: SlipType::Normal,
321            utxoset_key: [0; UTXO_KEY_LENGTH],
322            is_utxoset_key_set: false,
323        });
324        snapshot.slips.push(Slip {
325            public_key: [3; 33],
326            amount: 30,
327            slip_index: 3,
328            block_id: 3,
329            tx_ordinal: 3,
330            slip_type: SlipType::Normal,
331            utxoset_key: [0; UTXO_KEY_LENGTH],
332            is_utxoset_key_set: false,
333        });
334
335        let str = snapshot.to_string();
336        assert_ne!(str.len(), 0);
337
338        let result: Result<BalanceSnapshot, String> = str.try_into();
339        assert!(result.is_ok(), "{:?}", result.err().unwrap());
340
341        let snapshot2 = result.unwrap();
342        assert_eq!(snapshot.timestamp, snapshot2.timestamp);
343        assert_eq!(snapshot.latest_block_id, snapshot2.latest_block_id);
344        assert_eq!(snapshot.latest_block_hash, snapshot2.latest_block_hash);
345        assert_eq!(snapshot2.slips.len(), 3);
346
347        for (index, slip) in snapshot.slips.iter().enumerate() {
348            let slip2 = snapshot2.slips.get(index).unwrap();
349            assert_eq!(slip.public_key, slip2.public_key);
350            assert_eq!(slip.block_id, slip2.block_id);
351            assert_eq!(slip.tx_ordinal, slip2.tx_ordinal);
352            assert_eq!(slip.amount, slip2.amount);
353        }
354    }
355}