mina_tree/staged_ledger/
hash.rs

1use ark_ff::PrimeField;
2use mina_curves::pasta::Fp;
3use poseidon::hash::Inputs;
4use sha2::{Digest, Sha256};
5
6use crate::{
7    proofs::field::FieldWitness, scan_state::pending_coinbase::PendingCoinbase,
8    AppendToInputs as _, ToInputs,
9};
10
11/// Convert to/from OCaml strings, such as
12/// "u~\218kzX\228$\027qG\239\135\255:\143\171\186\011\200P\243\163\135\223T>\017\172\254\1906"
13pub trait OCamlString {
14    fn to_ocaml_str(&self) -> String;
15    fn from_ocaml_str(s: &str) -> Self;
16}
17
18impl<const N: usize> OCamlString for [u8; N] {
19    fn to_ocaml_str(&self) -> String {
20        to_ocaml_str(self)
21    }
22
23    fn from_ocaml_str(s: &str) -> Self {
24        from_ocaml_str(s)
25    }
26}
27
28pub fn to_ocaml_str(bytes: &[u8]) -> String {
29    let mut s = String::with_capacity(256);
30
31    for b in bytes {
32        let c = char::from(*b);
33        if c == '\\' {
34            s.push_str("\\\\");
35        } else if c.is_ascii() && !c.is_ascii_control() {
36            s.push(c);
37        } else {
38            match b {
39                7 => s.push_str(r"\a"),
40                8 => s.push_str(r"\b"),
41                9 => s.push_str(r"\t"),
42                10 => s.push_str(r"\n"),
43                // 11 => s.push_str(r"\v"),
44                12 => s.push_str(r"\f"),
45                13 => s.push_str(r"\r"),
46                _ => s.push_str(&format!("\\{:<03}", b)),
47            }
48        }
49    }
50
51    s
52}
53
54pub fn from_ocaml_str<const N: usize>(s: &str) -> [u8; N] {
55    let mut bytes = [0; N];
56    let mut b_index = 0;
57
58    let mut index = 0;
59    let s = s.as_bytes();
60    while index < s.len() {
61        if s[index] == b'\\' {
62            if s.get(index + 1).map(|next| *next == b'\\').unwrap_or(false) {
63                bytes[b_index] = b'\\';
64                index += 2;
65            } else if s
66                .get(index + 1)
67                .map(|next| "abtnfr".contains(char::from(*next)))
68                // .map(|next| "abtnvfr".contains(char::from(*next)))
69                .unwrap_or(false)
70            {
71                bytes[b_index] = match s[index + 1] {
72                    b'a' => 7,
73                    b'b' => 8,
74                    b't' => 9,
75                    b'n' => 10,
76                    // b'v' => 11,
77                    b'f' => 12,
78                    b'r' => 13,
79                    _ => unreachable!(),
80                };
81                index += 2;
82            } else {
83                let n1 = s[index + 1] - b'0';
84                let n2 = s[index + 2] - b'0';
85                let n3 = s[index + 3] - b'0';
86                bytes[b_index] = (n1 * 100) + (n2 * 10) + n3;
87                index += 4;
88            }
89        } else {
90            bytes[b_index] = s[index];
91            index += 1;
92        }
93
94        b_index += 1;
95    }
96
97    bytes
98}
99
100/// <https://github.com/MinaProtocol/mina/blob/3fe924c80a4d01f418b69f27398f5f93eb652514/src/lib/mina_base/staged_ledger_hash.ml#L27>
101#[derive(Clone, PartialEq, Eq)]
102pub struct AuxHash(pub [u8; 32]);
103
104impl std::fmt::Debug for AuxHash {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        f.write_fmt(format_args!("AuxHash({})", self.to_ocaml_str()))
107    }
108}
109
110impl AuxHash {
111    fn to_ocaml_str(&self) -> String {
112        to_ocaml_str(&self.0)
113    }
114
115    fn from_ocaml_str(s: &str) -> Self {
116        Self(from_ocaml_str(s))
117    }
118}
119
120/// <https://github.com/MinaProtocol/mina/blob/3fe924c80a4d01f418b69f27398f5f93eb652514/src/lib/mina_base/staged_ledger_hash.ml#L110>
121#[derive(Clone, PartialEq, Eq)]
122pub struct PendingCoinbaseAux(pub [u8; 32]);
123
124impl std::fmt::Debug for PendingCoinbaseAux {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        f.write_fmt(format_args!("PendingCoinbaseAux({})", self.to_ocaml_str()))
127    }
128}
129
130impl PendingCoinbaseAux {
131    fn to_ocaml_str(&self) -> String {
132        to_ocaml_str(&self.0)
133    }
134
135    fn from_ocaml_str(s: &str) -> Self {
136        Self(from_ocaml_str(s))
137    }
138}
139
140/// <https://github.com/MinaProtocol/mina/blob/3fe924c80a4d01f418b69f27398f5f93eb652514/src/lib/mina_base/staged_ledger_hash.ml#L152>
141#[derive(Clone, Debug, PartialEq, Eq)]
142pub struct NonStark {
143    pub ledger_hash: Fp,
144    pub aux_hash: AuxHash,
145    pub pending_coinbase_aux: PendingCoinbaseAux,
146}
147
148impl NonStark {
149    /// <https://github.com/MinaProtocol/mina/blob/3fe924c80a4d01f418b69f27398f5f93eb652514/src/lib/mina_base/staged_ledger_hash.ml#L182>
150    pub fn digest(&self) -> [u8; 32] {
151        let Self {
152            ledger_hash,
153            aux_hash,
154            pending_coinbase_aux,
155        } = self;
156
157        let mut sha: Sha256 = Sha256::new();
158
159        let mut ledger_hash_bytes: [u8; 32] = <[u8; 32]>::default();
160
161        let ledger_hash = ledger_hash.into_bigint();
162        // Convert [u64; 4] to bytes in little-endian
163        for (i, &word) in ledger_hash.0.iter().enumerate() {
164            let bytes = word.to_le_bytes();
165            ledger_hash_bytes[i * 8..(i + 1) * 8].copy_from_slice(&bytes);
166        }
167        ledger_hash_bytes.reverse();
168
169        sha.update(ledger_hash_bytes.as_slice());
170        sha.update(aux_hash.0.as_slice());
171        sha.update(pending_coinbase_aux.0.as_slice());
172
173        sha.finalize().into()
174    }
175}
176
177impl ToInputs for NonStark {
178    /// <https://github.com/MinaProtocol/mina/blob/3fe924c80a4d01f418b69f27398f5f93eb652514/src/lib/mina_base/staged_ledger_hash.ml#L193>
179    fn to_inputs(&self, inputs: &mut Inputs) {
180        let digest = self.digest();
181        inputs.append_bytes(digest.as_slice());
182    }
183}
184
185/// Staged ledger hash has two parts
186///
187/// 1) merkle root of the pending coinbases
188/// 2) ledger hash, aux hash, and the FIFO order of the coinbase stacks(Non snark).
189///
190/// Only part 1 is required for blockchain snark computation and therefore the
191/// remaining fields of the staged ledger are grouped together as "Non_snark"
192///
193/// <https://github.com/MinaProtocol/mina/blob/3fe924c80a4d01f418b69f27398f5f93eb652514/src/lib/mina_base/staged_ledger_hash.ml#L259>
194#[derive(Clone, Debug, PartialEq, Eq)]
195pub struct StagedLedgerHash<F: FieldWitness> {
196    pub non_snark: NonStark,
197    pub pending_coinbase_hash: F,
198}
199
200impl StagedLedgerHash<Fp> {
201    /// <https://github.com/MinaProtocol/mina/blob/3fe924c80a4d01f418b69f27398f5f93eb652514/src/lib/mina_base/staged_ledger_hash.ml#L290>
202    pub fn of_aux_ledger_and_coinbase_hash(
203        aux_hash: AuxHash,
204        ledger_hash: Fp,
205        pending_coinbase: &mut PendingCoinbase,
206    ) -> Self {
207        Self {
208            non_snark: NonStark {
209                ledger_hash,
210                aux_hash,
211                pending_coinbase_aux: pending_coinbase.hash_extra(),
212            },
213            pending_coinbase_hash: pending_coinbase.merkle_root(),
214        }
215    }
216
217    /// Used for tests only
218    #[cfg(test)]
219    pub fn from_ocaml_strings(
220        ledger_hash: &str,
221        aux_hash: &str,
222        pending_coinbase_aux: &str,
223        pending_coinbase_hash: &str,
224    ) -> Self {
225        use std::str::FromStr;
226
227        Self {
228            non_snark: NonStark {
229                ledger_hash: Fp::from_str(ledger_hash).unwrap(),
230                aux_hash: AuxHash::from_ocaml_str(aux_hash),
231                pending_coinbase_aux: PendingCoinbaseAux::from_ocaml_str(pending_coinbase_aux),
232            },
233            pending_coinbase_hash: Fp::from_str(pending_coinbase_hash).unwrap(),
234        }
235    }
236}
237
238impl ToInputs for StagedLedgerHash<Fp> {
239    fn to_inputs(&self, inputs: &mut Inputs) {
240        let Self {
241            non_snark,
242            pending_coinbase_hash,
243        } = self;
244
245        inputs.append(non_snark);
246        inputs.append(pending_coinbase_hash);
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use std::str::FromStr;
253
254    #[cfg(target_family = "wasm")]
255    use wasm_bindgen_test::wasm_bindgen_test as test;
256
257    use super::*;
258
259    #[test]
260    fn test_convert() {
261        // stage_ledger_hash=StagedLedgerHash {
262        //     non_snark: NonStark {
263        //         ledger_hash: Fp(7213023165825031994332898585791275635753820608093286100176380057570051468967),
264        //         aux_hash: AuxHash(\\\249\245k\176]TJ\216\183\001\204\177\131\030\244o\178\188\191US\156\192Hi\194P\223\004\000\003),
265        //         pending_coinbase_aux: PendingCoinbaseAux(\\\236\235f\255\200o8\217Rxlmily\194\219\1949\221N\145\180g)\215:'\251W\233),
266        //     },
267        //     pending_coinbase_hash: Fp(25504365445533103805898245102289650498571312278321176071043666991586378788150),
268        // }
269
270        let s = r"\\\236\235f\255\200o8\217Rxlmily\194\219\1949\221N\145\180g)\215:'\251W\233";
271        let pending_coinbase_aux = PendingCoinbaseAux::from_ocaml_str(s);
272        assert_eq!(s, pending_coinbase_aux.to_ocaml_str());
273
274        let s = r"\\\249\245k\176]TJ\216\183\001\204\177\131\030\244o\178\188\191US\156\192Hi\194P\223\004\000\003";
275        let aux_hash = AuxHash::from_ocaml_str(s);
276        assert_eq!(s, aux_hash.to_ocaml_str());
277
278        // non_snark digest="\t\204S\160F\227\022\142\146\172\220.R'\222L&b\191\138;\022\235\137\190>\205.\031\195-\231"
279        // digest=9,204,83,160,70,227,22,142,146,172,220,46,82,39,222,76,38,98,191,138,59,22,235,137,190,62,205,46,31,195,45,231
280
281        let a = AuxHash([
282            9, 204, 83, 160, 70, 227, 22, 142, 146, 172, 220, 46, 82, 39, 222, 76, 38, 98, 191,
283            138, 59, 22, 235, 137, 190, 62, 205, 46, 31, 195, 45, 231,
284        ]);
285
286        println!("a={}", a.to_ocaml_str());
287
288        let s = r"\t\204S\160F\227\022\142\146\172\220.R'\222L&b\191\138;\022\235\137\190>\205.\031\195-\231";
289        assert_eq!(s, a.to_ocaml_str());
290        let aux_hash = AuxHash::from_ocaml_str(s);
291        assert_eq!(s, aux_hash.to_ocaml_str());
292
293        let s = r"\000 \014WQ\192&\229C\178\232\171.\176`\153\218\161\209\229\223Gw\143w\135\250\171E\205\241/\227\168";
294        let memo = <[u8; 34]>::from_ocaml_str(s);
295        assert_eq!(s, memo.to_ocaml_str());
296
297        // let bytes = [10,220,211,153,14,65,191,6,19,231,47,244,155,5,212,131,48,124,227,133,176,79,196,131,23,116,152,178,130,63,206,85];
298
299        // let s = bytes.to_ocaml_str();
300        // println!("s='{}'", s);
301
302        let s = r"\n\220\211\153\014A\191\006\019\231/\244\155\005\212\1310|\227\133\176O\196\131\023t\152\178\130?\206U";
303        let pending_coinbase_aux = PendingCoinbaseAux::from_ocaml_str(s);
304        assert_eq!(s, pending_coinbase_aux.to_ocaml_str());
305    }
306
307    #[test]
308    fn test_non_snark_digest() {
309        // ((non_snark
310        //   ((ledger_hash
311        //     7213023165825031994332898585791275635753820608093286100176380057570051468967)
312        //    (aux_hash
313        //     "T\249\245k\176]TJ\216\183\001\204\177\131\030\244o\178\188\191US\156\192Hi\194P\223\004\000\003")
314        //    (pending_coinbase_aux
315        //     "_\236\235f\255\200o8\217Rxlmily\194\219\1949\221N\145\180g)\215:'\251W\233")))
316
317        let non_snark = NonStark {
318            ledger_hash: Fp::from_str(
319                "7213023165825031994332898585791275635753820608093286100176380057570051468967",
320            )
321            .unwrap(),
322            aux_hash: AuxHash::from_ocaml_str(
323                r"T\249\245k\176]TJ\216\183\001\204\177\131\030\244o\178\188\191US\156\192Hi\194P\223\004\000\003",
324            ),
325            pending_coinbase_aux: PendingCoinbaseAux::from_ocaml_str(
326                r"_\236\235f\255\200o8\217Rxlmily\194\219\1949\221N\145\180g)\215:'\251W\233",
327            ),
328        };
329
330        assert_eq!(
331            non_snark.digest().to_ocaml_str(),
332            r"\t\204S\160F\227\022\142\146\172\220.R'\222L&b\191\138;\022\235\137\190>\205.\031\195-\231"
333        );
334
335        // non_snark=((ledger_hash
336        //   18582860218764414485081234471609377222894570081548691702645303871998665679024)
337        //  (aux_hash
338        //   "0\136Wg\182DbX\203kLi\212%\199\206\142#\213`L\160bpCB\1413\240\193\171K")
339        //  (pending_coinbase_aux
340        //    "\
341        //   \n\220\211\153\014A\191\006\019\231/\244\155\005\212\1310|\227\133\176O\196\131\023t\152\178\130?\206U"))
342
343        let non_snark = NonStark {
344            ledger_hash: Fp::from_str(
345                "18582860218764414485081234471609377222894570081548691702645303871998665679024",
346            )
347            .unwrap(),
348            aux_hash: AuxHash::from_ocaml_str(
349                r"0\136Wg\182DbX\203kLi\212%\199\206\142#\213`L\160bpCB\1413\240\193\171K",
350            ),
351            pending_coinbase_aux: PendingCoinbaseAux::from_ocaml_str(
352                r"\n\220\211\153\014A\191\006\019\231/\244\155\005\212\1310|\227\133\176O\196\131\023t\152\178\130?\206U",
353            ),
354        };
355
356        assert_eq!(
357            non_snark.digest().to_ocaml_str(),
358            r"u~\218kzX\228$\027qG\239\135\255:\143\171\186\011\200P\243\163\135\223T>\017\172\254\1906",
359        );
360    }
361}