tezos_smart_rollup_encoding/
outbox.rs

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