1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
// Copyright (c) 2022-2023 The MobileCoin Foundation

use alloc::string::ToString;
use core::{ops::Deref, str::from_utf8};

use emstr::{helpers::Fractional, EncodeStr};
use prost::{
    bytes::{BufMut, BytesMut},
    Message,
};

use mc_core::account::{RingCtAddress, ShortAddressHash};

use crate::engine::{Error, TokenId};

mod schnorrkel;
pub use schnorrkel::sign_authority;

// Include generated protobuf types
include!(concat!(env!("OUT_DIR"), "/mob.rs"));

/// Per-token information
struct TokenInfo {
    pub id: u64,
    pub scalar: i64,
}

/// Token information for rendering / display
const TOKENS: &[TokenInfo] = &[
    TokenInfo {
        id: 0,
        scalar: SCALAR_MOB,
    },
    TokenInfo {
        id: 1,
        scalar: 1_000_000,
    },
];

const SCALAR_MOB: i64 = 1_000_000_000_000;
const MOB_MAX_SF: usize = 14;

fn get_token_info(token_id: TokenId) -> Option<&'static TokenInfo> {
    TOKENS.iter().find(|&t| t.id == *token_id.deref())
}

// Format helper for values and token types
pub fn fmt_token_val(value: i64, token_id: TokenId, buff: &mut [u8]) -> &str {
    // Match token types
    let scalar = get_token_info(token_id).map(|v| v.scalar).unwrap_or(1);

    // Compute and write value using scalar
    let mut n = match emstr::write!(&mut buff[..], Fractional::<i64>::new(value, scalar)) {
        Ok(v) => v,
        Err(_) => return "ENCODE_ERR",
    };

    // Backtrack and truncate values if max chars is exceeded
    if n > MOB_MAX_SF {
        n = MOB_MAX_SF;
        buff[n] = b'.';
        buff[n + 1] = b'.';
        n += 2;
    }

    // Write token type
    let r = match token_id.deref() {
        // NOTE THAT NAMES STRINGS MUST BE HARDCODED TO AVOID PIC issues with the ledger
        0 => emstr::write!(&mut buff[n..], " MOB"),
        1 => emstr::write!(&mut buff[n..], " eUSD"),
        _ => emstr::write!(&mut buff[n..], " (", token_id.deref(), ')'),
    };
    match r {
        Ok(v) => n += v,
        Err(_) => return "ENCODE_ERR",
    }

    // TODO: ensure values can not be concatenated

    match from_utf8(&buff[..n]) {
        Ok(v) => v,
        Err(_) => "INVALID_UTF8",
    }
}

/// Helper to digest PublicAddress equivalents for [ShortAddressHash]
/// without requiring mc-account-keys (or alloc / bytes).
///
/// This is a re-implementation of the derived [Digestible] for PublicAddress, with tests to ensure these remain in-sync.
#[cfg_attr(feature = "noinline", inline(never))]
pub(crate) fn digest_public_address(
    subaddress: impl RingCtAddress,
    fog_report_url: &str,
    fog_authority_sig: &[u8],
) -> ShortAddressHash {
    use mc_crypto_digestible::{DigestTranscript, Digestible, MerlinTranscript};

    // Setup transcript
    let context = b"mc-address";
    let mut transcript = <MerlinTranscript as DigestTranscript>::new();

    // Write [PublicAddress] equivalent transcript
    transcript.append_agg_header(context, "PublicAddress".as_bytes());
    subaddress
        .view_public_key()
        .inner()
        .append_to_transcript_allow_omit("view_public_key".as_bytes(), &mut transcript);
    subaddress
        .spend_public_key()
        .inner()
        .append_to_transcript_allow_omit("spend_public_key".as_bytes(), &mut transcript);
    fog_report_url.append_to_transcript("fog_report_url".as_bytes(), &mut transcript);
    r#""#.append_to_transcript("fog_report_id".as_bytes(), &mut transcript);
    fog_authority_sig.append_to_transcript("fog_authority_sig".as_bytes(), &mut transcript);
    transcript.append_agg_closer(context, "PublicAddress".as_bytes());

    // Extract digest
    let mut digest = [0u8; 32];
    transcript.extract_digest(&mut digest);

    // Return first 16 bytes as ShortAddressHash
    let hash: [u8; 16] = digest[0..16].try_into().expect("arithmetic error");
    ShortAddressHash::from(hash)
}

/// Helper to b58 encode [PublicAddress] equivalent types without
/// pulling in no-std incompatible `mc_api` dependency.
#[cfg_attr(feature = "noinline", inline(never))]
pub fn b58_encode_public_address<const N: usize>(
    subaddress: impl RingCtAddress,
    fog_report_url: &str,
    fog_authority_sig: &[u8],
) -> Result<heapless::String<N>, Error> {
    use printable_wrapper::*;

    let view_public = subaddress.view_public_key();
    let spend_public = subaddress.spend_public_key();

    // Build printable protobuf wrapper
    // NOTE: this uses prost / is heavily alloc'd under the hood
    let p = PrintableWrapper {
        wrapper: Some(Wrapper::PublicAddress(PublicAddress {
            view_public_key: Some(CompressedRistretto {
                data: view_public.to_bytes().to_vec(),
            }),
            spend_public_key: Some(CompressedRistretto {
                data: spend_public.to_bytes().to_vec(),
            }),
            fog_report_url: fog_report_url.to_string(),
            fog_report_id: "".to_string(),
            fog_authority_sig: fog_authority_sig.to_vec(),
        })),
    };

    // Encode to temporary byte buffer
    // TODO: prefer not to use alloc here but, BufMut seems to be broken for
    // const generic types?!
    let mut data = BytesMut::with_capacity(p.encoded_len() + 4);
    // Pre-allocate space for checksum
    data.put_bytes(0, 4);
    // Encode proto to buffer
    p.encode(&mut data).unwrap();

    // Force drop `p` to free up heap memory
    drop(p);

    // Compute checksum for encoded address
    let checksum = crc::Crc::<u32>::new(&crc::CRC_32_ISO_HDLC)
        .checksum(&data[4..])
        .to_le_bytes();

    // Write checksum to start of buffer
    data[0..4].copy_from_slice(&checksum);

    // Encode address to b58
    let mut buff = HeaplessEncodeTarget::<N>(heapless::String::new());
    let _n = bs58::encode(&data).into(&mut buff).unwrap();

    Ok(buff.0)
}

/// Helper to support bs58 encoding to [heapless::String] types
struct HeaplessEncodeTarget<const N: usize>(heapless::String<N>);

impl<const N: usize> bs58::encode::EncodeTarget for HeaplessEncodeTarget<N> {
    fn encode_with(
        &mut self,
        max_len: usize,
        f: impl for<'a> FnOnce(&'a mut [u8]) -> bs58::encode::Result<usize>,
    ) -> bs58::encode::Result<usize> {
        // Fetch vec and resize
        let v = unsafe { self.0.as_mut_vec() };
        v.resize_default(max_len)
            .map_err(|_| bs58::encode::Error::BufferTooSmall)?;

        // Encode into resized vec
        let n = match f(&mut v[..]) {
            Ok(n) => n,
            Err(e) => {
                // On encoding failure clear vec to avoid returning
                // invalid utf8 string
                v.clear();
                return Err(e);
            }
        };

        // Truncate down to encoded len
        v.truncate(n);

        Ok(n)
    }
}

#[cfg(test)]
mod test {
    use mc_account_keys::AccountKey;
    use mc_transaction_types::TokenId;
    use rand_core::OsRng;

    use super::*;
    use crate::engine::{FogCert, FogId};

    pub(crate) const MAX_LINE_LEN: usize = 20;

    #[test]
    fn fmt_mob() {
        let tests = &[
            (1, "0.000000000001 MOB"),
            (10_000_000, "0.00001 MOB"),
            (10_020_000, "0.00001002 MOB"),
            (10_000_001, "0.000010000001 MOB"),
            (40_000_000, "0.00004 MOB"),
            (40_030_000, "0.00004003 MOB"),
            (-40_000_000, "-0.00004 MOB"),
            (-40_040_000, "-0.00004004 MOB"),
            (400 * SCALAR_MOB, "400 MOB"),
            (400 * SCALAR_MOB + 10_000, "400.00000001 MOB"),
            (400 * SCALAR_MOB + 1, "400.0000000000.. MOB"),
        ];

        for (v, s) in tests {
            let mut buff = [0u8; 32];

            let e = fmt_token_val(*v, TokenId::MOB, &mut buff);

            assert_eq!(&e, s);
            assert!(
                e.len() <= MAX_LINE_LEN,
                "length {} exceeds line limit {} for {}",
                e.len(),
                MAX_LINE_LEN,
                s
            );
        }
    }

    const FOGS: &[FogId] = &[
        FogId::MobMain,
        FogId::MobTest,
        FogId::SignalMain,
        FogId::SignalTest,
    ];

    #[test]
    fn short_address_hash_no_fog() {
        for _i in 0..10 {
            // Create random account without fog info
            let a = AccountKey::random(&mut OsRng {});

            // Test short address hashing
            let h1 = ShortAddressHash::from(&a.default_subaddress());
            let h2 = digest_public_address(a.default_subaddress(), "", &[]);

            assert_eq!(h1, h2);
        }
    }

    #[test]
    fn short_address_hash_with_fog() {
        for f in FOGS {
            for _i in 0..10 {
                // Create random account with fog info
                let a = AccountKey::random(&mut OsRng {}).with_fog(f.url(), "", f.spki());
                let p = a.default_subaddress();

                // Test short address hashing
                let h1 = ShortAddressHash::from(&p);
                let h2 = digest_public_address(
                    &p,
                    p.fog_report_url().unwrap_or(""),
                    p.fog_authority_sig().unwrap_or(&[]),
                );

                assert_eq!(h1, h2);
            }
        }
    }

    const B58_MAX_LEN: usize = 512;

    #[test]
    fn b58_address_no_fog() {
        for _i in 0..10 {
            // Create random account without fog info
            let a = AccountKey::random(&mut OsRng {});
            let p = a.default_subaddress();

            // Local b58 encoding
            let s1 = b58_encode_public_address::<B58_MAX_LEN>(
                &p,
                p.fog_report_url().unwrap_or(""),
                p.fog_authority_sig().unwrap_or(&[]),
            )
            .unwrap();

            // API standard b58 encoding
            let mut wrapper = mc_api::printable::PrintableWrapper::new();
            wrapper.set_public_address((&p).into());
            let s2 = wrapper.b58_encode().unwrap();

            assert_eq!(s1.as_str(), s2.as_str());
        }
    }

    #[test]
    fn b58_address_with_fog() {
        for f in FOGS {
            for _i in 0..10 {
                // Create random account with fog info
                let a = AccountKey::random(&mut OsRng {}).with_fog(f.url(), "", f.spki());
                let p = a.default_subaddress();

                // Local b58 encoding
                let s1 = b58_encode_public_address::<B58_MAX_LEN>(
                    &p,
                    p.fog_report_url().unwrap_or(""),
                    p.fog_authority_sig().unwrap_or(&[]),
                )
                .unwrap();

                // API standard b58 encoding
                let mut wrapper = mc_api::printable::PrintableWrapper::new();
                wrapper.set_public_address((&p).into());
                let s2 = wrapper.b58_encode().unwrap();

                assert_eq!(s1.as_str(), s2.as_str());
            }
        }
    }
}