mavryk_smart_rollup_encoding/
outbox.rs

1// SPDX-FileCopyrightText: 2022-2024 TriliTech <contact@trili.tech>
2// SPDX-FileCopyrightText: 2023 Nomadic Labs <contact@nomadic-labs.com>
3//
4// SPDX-License-Identifier: MIT
5
6//! Types & encodings for the *outbox-half* of the *L1/L2 communication protocol*
7//!
8//! In *general*, this module is a re-implementation of the mavryk-protocol
9//! [outbox message repr].
10//!
11//! We have, however, a *specialised* [OutboxMessageTransaction::parameters]. The
12//! communication protocol allows payload to be any michelson `script_expr`.
13//!
14//! [outbox message repr]: <https://gitlab.com/tezos/tezos/-/blob/80b2cccb9c663dde2d86a6c94806fc149b7d1ef3/src/proto_alpha/lib_protocol/sc_rollup_outbox_message_repr.ml>
15
16use mavryk_data_encoding::enc::BinWriter;
17use mavryk_data_encoding::encoding::HasEncoding;
18use mavryk_data_encoding::nom::NomReader;
19
20use crate::contract::Contract;
21use crate::entrypoint::Entrypoint;
22use crate::michelson::Michelson;
23#[cfg(feature = "proto-alpha")]
24use crate::public_key_hash::PublicKeyHash;
25
26/// Outbox message, sent by the kernel as mavryk-encoded bytes.
27///
28/// Encoded as a dynamic list of [OutboxMessageTransaction], with **no** case tag.
29#[derive(Debug, PartialEq, Eq, HasEncoding, NomReader, BinWriter)]
30pub enum OutboxMessage<Expr: Michelson> {
31    /// List of outbox transactions that must succeed together.
32    #[encoding(tag = 0)]
33    AtomicTransactionBatch(OutboxMessageTransactionBatch<Expr>),
34    /// Only keys in the whitelist are allowed to stake and publish a commitment.
35    #[cfg(feature = "proto-alpha")]
36    #[encoding(tag = 2)]
37    WhitelistUpdate(OutboxMessageWhitelistUpdate),
38}
39
40/// A batch of [`OutboxMessageTransaction`].
41#[derive(Debug, PartialEq, Eq, HasEncoding, BinWriter, NomReader)]
42pub struct OutboxMessageTransactionBatch<Expr: Michelson> {
43    #[encoding(dynamic, list)]
44    batch: Vec<OutboxMessageTransaction<Expr>>,
45}
46
47impl<Expr: Michelson> OutboxMessageTransactionBatch<Expr> {
48    /// Returns the number of transactions in the batch.
49    pub fn len(&self) -> usize {
50        self.batch.len()
51    }
52
53    /// Returns whether the batch is empty.
54    pub fn is_empty(&self) -> bool {
55        self.batch.is_empty()
56    }
57}
58
59impl<Expr: Michelson> core::ops::Index<usize> for OutboxMessageTransactionBatch<Expr> {
60    type Output = OutboxMessageTransaction<Expr>;
61
62    fn index(&self, index: usize) -> &Self::Output {
63        self.batch.index(index)
64    }
65}
66
67impl<Expr: Michelson> From<Vec<OutboxMessageTransaction<Expr>>>
68    for OutboxMessageTransactionBatch<Expr>
69{
70    fn from(batch: Vec<OutboxMessageTransaction<Expr>>) -> Self {
71        Self { batch }
72    }
73}
74
75impl<Expr: Michelson> From<OutboxMessageTransaction<Expr>> for OutboxMessage<Expr> {
76    fn from(transaction: OutboxMessageTransaction<Expr>) -> Self {
77        Self::AtomicTransactionBatch(vec![transaction].into())
78    }
79}
80
81impl<Expr: Michelson> From<Vec<OutboxMessageTransaction<Expr>>> for OutboxMessage<Expr> {
82    fn from(batch: Vec<OutboxMessageTransaction<Expr>>) -> Self {
83        Self::AtomicTransactionBatch(batch.into())
84    }
85}
86
87/// Outbox message transaction, part of the outbox message.
88///
89/// Encoded as:
90/// ```ocaml
91/// (obj3
92///   (req "parameters" Script_repr.expr_encoding)
93///   (req "destination" Contract_repr.originated_encoding)
94///   (req "entrypoint" Entrypoint_repr.simple_encoding))
95/// ```
96#[derive(Debug, PartialEq, Eq, HasEncoding, BinWriter, NomReader)]
97pub struct OutboxMessageTransaction<Expr: Michelson> {
98    /// Micheline-encoded payload, sent to the destination contract.
99    pub parameters: Expr,
100    /// The destination smart-contract.
101    ///
102    /// Protocol side this is a `Contract_hash` (aka `ContractKT1Hash`), but encoded as
103    /// `Contract.originated_encoding`.
104    pub destination: Contract,
105    /// The entrypoint of the destination that will be called.
106    pub entrypoint: Entrypoint,
107}
108
109/// Whitelist update, part of the outbox message.
110/// The keys in the whitelist are allowed to stake and publish a commitment.
111/// The whitelist is either Some (non empty list), or None (remove the whitelist,
112/// allowing anyone to stake/publish commitments).
113///
114/// Encoded as:
115/// `(opt "whitelist" Sc_rollup_whitelist_repr.encoding)`
116/// where
117/// `encoding = Data_encoding.(list Signature.Public_key_hash.encoding)`
118#[cfg(feature = "proto-alpha")]
119#[derive(Debug, PartialEq, Eq, HasEncoding, BinWriter, NomReader)]
120pub struct OutboxMessageWhitelistUpdate {
121    /// The new contents of the whitelist
122    #[encoding(dynamic, list)]
123    pub whitelist: Option<Vec<PublicKeyHash>>,
124}
125
126/// Kinds of invalid whitelists
127#[cfg(feature = "proto-alpha")]
128#[derive(Debug, PartialEq, Eq)]
129pub enum InvalidWhitelist {
130    /// If the provided whitelist is the empty list
131    EmptyWhitelist,
132    /// Duplicated keys
133    DuplicatedKeys,
134}
135
136#[cfg(feature = "proto-alpha")]
137fn has_unique_elements<T>(iter: T) -> bool
138where
139    T: IntoIterator,
140    T::Item: Eq + Ord,
141{
142    let mut uniq = std::collections::BTreeSet::new();
143    iter.into_iter().all(move |x| uniq.insert(x))
144}
145
146/// Returns `Err` on empty list, to not accidentally set the whitelist to
147/// None, thereby making the rollup public.
148#[cfg(feature = "proto-alpha")]
149impl TryFrom<Option<Vec<PublicKeyHash>>> for OutboxMessageWhitelistUpdate {
150    type Error = InvalidWhitelist;
151
152    fn try_from(whitelist: Option<Vec<PublicKeyHash>>) -> Result<Self, Self::Error> {
153        match whitelist {
154            Some(mut list) => {
155                if list.is_empty() {
156                    return Err(InvalidWhitelist::EmptyWhitelist);
157                };
158                if !has_unique_elements(&mut list) {
159                    Err(InvalidWhitelist::DuplicatedKeys)
160                } else {
161                    Ok(Self {
162                        whitelist: Some(list),
163                    })
164                }
165            }
166            None => Ok(Self { whitelist: None }),
167        }
168    }
169}
170
171#[cfg(test)]
172mod test {
173    use crate::michelson::ticket::StringTicket;
174
175    use super::*;
176
177    // first byte is union tag (currently always `0`, next four are list size of batch)
178    const ENCODED_OUTBOX_MESSAGE_PREFIX: [u8; 5] = [0, 0, 0, 0, 152];
179
180    // first byte is union tag (currently always `2`)
181    #[cfg(feature = "proto-alpha")]
182    const ENCODED_OUTBOX_MESSAGE_WHITELIST_PREFIX: [u8; 1] = [2];
183
184    const ENCODED_TRANSACTION_ONE: [u8; 74] = [
185        7, 7, // Prim pair tags
186        // Contract KT1 hash
187        b'\n', 0, 0, 0, 22, 1, 209, 163, b'|', 8, 138, 18, b'!', 182, b'6', 187, b'_',
188        204, 179, b'^', 5, 24, 16, b'8', 186, b'|', 0, // Padding
189        7, 7, // Prim pair tags,
190        // String
191        1, // tag
192        0, 0, 0, 3, // size,
193        b'r', b'e', b'd', // contents,
194        0, 1, // nat size + value
195        // Destination
196        1, // originated tag
197        36, 102, 103, 169, 49, 254, 11, 210, 251, 28, 182, 4, 247, 20, 96, 30, 136, 40,
198        69, 80, // end originated
199        0,  // padding
200        //  entrypoint
201        0, 0, 0, 7, b'd', b'e', b'f', b'a', b'u', b'l', b't',
202    ];
203
204    const ENCODED_TRANSACTION_TWO: [u8; 78] = [
205        // String Ticket
206        7, 7, // prim pair tags
207        // Contract KT1 hash
208        b'\n', 0, 0, 0, 22, 1, b'$', b'f', b'g', 169, b'1', 254, 11, 210, 251, 28, 182, 4,
209        247, 20, b'`', 30, 136, b'(', b'E', b'P', 0, // padding
210        7, 7, // prim pair tags
211        // String contents
212        1, 0, 0, 0, 6, b'y', b'e', b'l', b'l', b'o', b'w', // end contents
213        0,    // Nat tag
214        137, 5, // Z(329)
215        // Destination
216        1, 21, 237, 173, b'\'', 159, b'U', 226, 254, b'@', 17, 222, b'm', b',', b'$', 253,
217        245, 27, 242, b'%', 197, 0, // Entrypoint
218        0, 0, 0, 7, // Entrypoint size
219        b'a', b'n', b'o', b't', b'h', b'e', b'r', // Entrypoint name
220    ];
221
222    // To display the encoding from OCaml:
223    // Format.asprintf "%a"
224    // Binary_schema.pp
225    // (Binary.describe (list Mavryk_crypto.Signature.Public_key_hash.encoding))
226    #[cfg(feature = "proto-alpha")]
227    const ENCODED_WHITELIST_UPDATE: [u8; 47] = [
228        0xff, // provide whitelist (0x0 for none)
229        0x0, 0x0, 0x0, 0x2a, // # bytes in next field
230        // sequence of public_key_hash (21 bytes, 8-bit tag)
231        // mv18Cw7psUrAAPBpXYd9CtCpHg9EgjHP9KTe
232        0x0, // Ed25519 (tag 0)
233        0x2, 0x29, 0x8c, 0x3, 0xed, 0x7d, 0x45, 0x4a, 0x10, 0x1e, 0xb7, 0x2, 0x2b, 0xc9,
234        0x5f, 0x7e, 0x5f, 0x41, 0xac, 0x78,
235        // mv1V73YiKvinVumxwvYWjCZBoT44wqBNhta7
236        0x0, // Ed25519 (tag 0)
237        0xe7, 0x67, 0xf, 0x32, 0x3, 0x81, 0x7, 0xa5, 0x9a, 0x2b, 0x9c, 0xfe, 0xfa, 0xe3,
238        0x6e, 0xa2, 0x1f, 0x5a, 0xa6, 0x3c,
239    ];
240
241    #[test]
242    fn encode_transaction() {
243        let mut bin = vec![];
244        transaction_one().bin_write(&mut bin).unwrap();
245        assert_eq!(&ENCODED_TRANSACTION_ONE, bin.as_slice());
246    }
247
248    #[test]
249    #[cfg(feature = "proto-alpha")]
250    fn encode_whitelist_update() {
251        let mut bin = vec![];
252        whitelist().bin_write(&mut bin).unwrap();
253        assert_eq!(&ENCODED_WHITELIST_UPDATE, bin.as_slice());
254    }
255
256    #[test]
257    fn decode_transaction() {
258        let (remaining, decoded) =
259            OutboxMessageTransaction::nom_read(ENCODED_TRANSACTION_TWO.as_slice())
260                .unwrap();
261
262        assert!(remaining.is_empty());
263        assert_eq!(transaction_two(), decoded);
264    }
265
266    #[test]
267    #[cfg(feature = "proto-alpha")]
268    fn decode_whitelist_update() {
269        let (remaining, decoded) =
270            OutboxMessageWhitelistUpdate::nom_read(ENCODED_WHITELIST_UPDATE.as_slice())
271                .unwrap();
272        assert!(remaining.is_empty());
273        assert_eq!(whitelist(), decoded);
274    }
275
276    #[test]
277    fn encode_outbox_message() {
278        let mut expected = ENCODED_OUTBOX_MESSAGE_PREFIX.to_vec();
279        expected.extend_from_slice(ENCODED_TRANSACTION_ONE.as_slice());
280        expected.extend_from_slice(ENCODED_TRANSACTION_TWO.as_slice());
281
282        let message = OutboxMessage::AtomicTransactionBatch(
283            vec![transaction_one(), transaction_two()].into(),
284        );
285
286        let mut bin = vec![];
287        message.bin_write(&mut bin).unwrap();
288
289        assert_eq!(expected, bin);
290    }
291
292    #[test]
293    #[cfg(feature = "proto-alpha")]
294    fn encode_outbox_message_whitelist() {
295        let mut expected = ENCODED_OUTBOX_MESSAGE_WHITELIST_PREFIX.to_vec();
296        expected.extend_from_slice(ENCODED_WHITELIST_UPDATE.as_slice());
297
298        let message: OutboxMessage<StringTicket> =
299            OutboxMessage::WhitelistUpdate(whitelist());
300
301        let mut bin = vec![];
302        message.bin_write(&mut bin).unwrap();
303
304        assert_eq!(expected, bin);
305    }
306
307    #[test]
308    fn decode_outbox_message() {
309        let mut bytes = ENCODED_OUTBOX_MESSAGE_PREFIX.to_vec();
310        bytes.extend_from_slice(ENCODED_TRANSACTION_TWO.as_slice());
311        bytes.extend_from_slice(ENCODED_TRANSACTION_ONE.as_slice());
312
313        let expected = OutboxMessage::AtomicTransactionBatch(
314            vec![transaction_two(), transaction_one()].into(),
315        );
316
317        let (remaining, message) = OutboxMessage::nom_read(bytes.as_slice()).unwrap();
318
319        assert!(remaining.is_empty());
320        assert_eq!(expected, message);
321    }
322
323    #[test]
324    #[cfg(feature = "proto-alpha")]
325    fn decode_outbox_message_whitelist() {
326        let mut bytes = ENCODED_OUTBOX_MESSAGE_WHITELIST_PREFIX.to_vec();
327        bytes.extend_from_slice(ENCODED_WHITELIST_UPDATE.as_slice());
328
329        let expected: OutboxMessage<StringTicket> =
330            OutboxMessage::WhitelistUpdate(whitelist());
331
332        let (remaining, message) = OutboxMessage::nom_read(bytes.as_slice()).unwrap();
333
334        assert!(remaining.is_empty());
335        assert_eq!(expected, message);
336    }
337
338    #[test]
339    fn decode_outbox_message_err_on_invalid_prefix() {
340        let mut bytes = ENCODED_OUTBOX_MESSAGE_PREFIX.to_vec();
341        // prefix too long, missing message
342        bytes.extend_from_slice(ENCODED_TRANSACTION_ONE.as_slice());
343        // garbage (to be ignored) tail
344        bytes.extend_from_slice([10; 1000].as_slice());
345
346        assert!(OutboxMessage::<StringTicket>::nom_read(bytes.as_slice()).is_err());
347    }
348
349    #[test]
350    #[cfg(feature = "proto-alpha")]
351    fn decode_outbox_message_whitelist_err_on_invalid_prefix() {
352        let mut bytes = ENCODED_OUTBOX_MESSAGE_PREFIX.to_vec();
353        // prefix too long, missing message
354        bytes.extend_from_slice(ENCODED_WHITELIST_UPDATE.as_slice());
355        // garbage (to be ignored) tail
356        bytes.extend_from_slice([10; 1000].as_slice());
357
358        assert!(OutboxMessage::<StringTicket>::nom_read(bytes.as_slice()).is_err());
359    }
360
361    fn transaction_one() -> OutboxMessageTransaction<StringTicket> {
362        let ticket = StringTicket::new(
363            Contract::from_b58check("KT1ThEdxfUcWUwqsdergy3QnbCWGHSUHeHJq").unwrap(),
364            "red".to_string(),
365            1_u64,
366        )
367        .unwrap();
368        make_transaction(ticket, "KT1BuEZtb68c1Q4yjtckcNjGELqWt56Xyesc", "default")
369    }
370
371    fn transaction_two() -> OutboxMessageTransaction<StringTicket> {
372        let ticket = StringTicket::new(
373            Contract::from_b58check("KT1BuEZtb68c1Q4yjtckcNjGELqWt56Xyesc").unwrap(),
374            "yellow".to_string(),
375            329_u64,
376        )
377        .unwrap();
378        make_transaction(ticket, "KT1AaiUqbT3NmQts2w7ofY4vJviVchztiW4y", "another")
379    }
380
381    fn make_transaction(
382        ticket: StringTicket,
383        destination: &str,
384        entrypoint: &str,
385    ) -> OutboxMessageTransaction<StringTicket> {
386        let parameters = ticket;
387        let destination = Contract::from_b58check(destination).unwrap();
388        let entrypoint = Entrypoint::try_from(entrypoint.to_string()).unwrap();
389
390        OutboxMessageTransaction {
391            parameters,
392            destination,
393            entrypoint,
394        }
395    }
396
397    #[cfg(feature = "proto-alpha")]
398    fn whitelist() -> OutboxMessageWhitelistUpdate {
399        let whitelist = Some(vec![
400            PublicKeyHash::from_b58check("mv18Cw7psUrAAPBpXYd9CtCpHg9EgjHP9KTe").unwrap(),
401            PublicKeyHash::from_b58check("mv1V73YiKvinVumxwvYWjCZBoT44wqBNhta7").unwrap(),
402        ]);
403        OutboxMessageWhitelistUpdate { whitelist }
404    }
405
406    #[test]
407    #[cfg(feature = "proto-alpha")]
408    fn tryfrom_whitelist() {
409        let addr =
410            PublicKeyHash::from_b58check("mv18Cw7psUrAAPBpXYd9CtCpHg9EgjHP9KTe").unwrap();
411        let l1 = Some(vec![addr.clone(), addr.clone()]);
412        let w1: Result<OutboxMessageWhitelistUpdate, _> = l1.try_into();
413        assert_eq!(
414            w1.expect_err("Expected Err(InvalidWhitelist::DuplicatedKeys)"),
415            InvalidWhitelist::DuplicatedKeys
416        );
417        let l2 = Some(vec![]);
418        let w2: Result<OutboxMessageWhitelistUpdate, _> = l2.try_into();
419        assert_eq!(
420            w2.expect_err("Expected Err(InvalidWhitelist::EmptyWhitelist)"),
421            InvalidWhitelist::EmptyWhitelist
422        );
423        let l3 = None;
424        let w3: Result<OutboxMessageWhitelistUpdate, _> = l3.try_into();
425        assert_eq!(
426            w3.expect("Expected Ok(message)"),
427            (OutboxMessageWhitelistUpdate { whitelist: None })
428        );
429        let l4 = Some(vec![addr]);
430        let w4: Result<OutboxMessageWhitelistUpdate, _> = l4.clone().try_into();
431        assert_eq!(
432            w4.expect("Expected Ok(message)"),
433            (OutboxMessageWhitelistUpdate { whitelist: l4 })
434        );
435    }
436}