diff --git a/src/kernel_sdk/encoding/src/inbox.rs b/src/kernel_sdk/encoding/src/inbox.rs index 2a9d64883e37cb7fc2d637bf732fb370fee1e071..dffe9cbbb6f1080443afbf381b31034f50ac74d7 100644 --- a/src/kernel_sdk/encoding/src/inbox.rs +++ b/src/kernel_sdk/encoding/src/inbox.rs @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: 2022-2023 TriliTech -// SPDX-FileCopyrightText: 2022-2023 Nomadic Labs +// SPDX-FileCopyrightText: 2022-2025 Nomadic Labs // SPDX-FileCopyrightText: 2023 Marigold // // SPDX-License-Identifier: MIT @@ -18,14 +18,17 @@ use crate::timestamp::Timestamp; use crypto::hash::{BlockHash, ContractKt1Hash}; use nom::bytes::complete::tag; use nom::combinator::{map, rest}; +use nom::number::complete::{be_i32, be_u16, be_u32}; use nom::sequence::pair; use nom::sequence::preceded; use nom::Finish; +use std::collections::BTreeMap; use std::fmt::Display; use tezos_data_encoding::enc; use tezos_data_encoding::enc::BinWriter; use tezos_data_encoding::encoding::HasEncoding; use tezos_data_encoding::nom::NomReader; +use tezos_data_encoding::types::Zarith; #[derive(Debug, PartialEq, Eq, NomReader, HasEncoding, BinWriter)] enum InboxMessageRepr { @@ -124,17 +127,161 @@ impl Display for InfoPerLevel { } } -/// Internal inbox message - known to be sent by the protocol +/// DAL attested slots message - contains information about which slots were +/// attested for a given published level, grouped by publisher. +/// +/// The slots for each publisher are encoded as a bitset (Z.t in OCaml), +/// where bit i is set if slot index i is attested. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct DalAttestedSlots { + /// The level at which the slots were published. + pub published_level: i32, + /// The number of DAL slots (used for interpreting the bitset). + pub number_of_slots: u16, + /// The size of each DAL slot in bytes. + pub slot_size: i32, + /// The size of each DAL page in bytes. + pub page_size: u16, + /// Map from publisher public key hash to their attested slots as a bitset. + /// Each Zarith value is a bitset where bit i is set if slot index i is attested. + pub slots_by_publisher: BTreeMap, +} + +// Manual NomReader implementation for DalAttestedSlots +// (BTreeMap doesn't work with derive macros) +impl NomReader<'_> for DalAttestedSlots { + fn nom_read(input: &[u8]) -> tezos_data_encoding::nom::NomResult { + // Parse published_level (int32) + let (input, published_level) = be_i32(input)?; + + // Parse number_of_slots (uint16) + let (input, number_of_slots) = be_u16(input)?; + + // Parse slot_size (int31, encoded as int32) + let (input, slot_size) = be_i32(input)?; + + // Parse page_size (uint16) + let (input, page_size) = be_u16(input)?; + + // Parse slots_by_publisher map: encoded as a sequence of (pkh, bitset) pairs + // with a 4-byte length prefix for the number of entries + let (input, num_entries) = be_u32(input)?; + + let mut slots_by_publisher = BTreeMap::new(); + let mut remaining = input; + + for _ in 0..num_entries { + // Parse public key hash + let (input, pkh) = PublicKeyHash::nom_read(remaining)?; + + // Parse bitset as Zarith (Z.t in OCaml, using Data_encoding.z) + let (input, bitset) = Zarith::nom_read(input)?; + + slots_by_publisher.insert(pkh, bitset); + remaining = input; + } + + Ok(( + remaining, + DalAttestedSlots { + published_level, + number_of_slots, + slot_size, + page_size, + slots_by_publisher, + }, + )) + } +} + +// Manual BinWriter implementation for DalAttestedSlots +impl BinWriter for DalAttestedSlots { + fn bin_write(&self, output: &mut Vec) -> enc::BinResult { + // Write published_level (int32, big-endian) + output.extend_from_slice(&self.published_level.to_be_bytes()); + + // Write number_of_slots (uint16, big-endian) + output.extend_from_slice(&self.number_of_slots.to_be_bytes()); + + // Write slot_size (int31, encoded as int32, big-endian) + output.extend_from_slice(&self.slot_size.to_be_bytes()); + + // Write page_size (uint16, big-endian) + output.extend_from_slice(&self.page_size.to_be_bytes()); + + // Write slots_by_publisher map: number of entries followed by (pkh, bitset) pairs + let num_entries = self.slots_by_publisher.len() as u32; + output.extend_from_slice(&num_entries.to_be_bytes()); + + for (pkh, bitset) in &self.slots_by_publisher { + pkh.bin_write(output)?; + bitset.bin_write(output)?; + } + + Ok(()) + } +} + +// Manual HasEncoding implementation for DalAttestedSlots +// Required for the derive macro on InternalInboxMessage to work +impl HasEncoding for DalAttestedSlots { + fn encoding() -> tezos_data_encoding::encoding::Encoding { + use tezos_data_encoding::encoding::{Encoding, Field}; + Encoding::Obj( + "DalAttestedSlots", + vec![ + Field::new("published_level", Encoding::Int32), + Field::new("number_of_slots", Encoding::Uint16), + Field::new("slot_size", Encoding::Int31), + Field::new("page_size", Encoding::Uint16), + Field::new( + "slots_by_publisher", + // Dynamic encoding for the map (length-prefixed sequence of pairs) + Encoding::dynamic(Encoding::list(Encoding::Tup(vec![ + PublicKeyHash::encoding(), + Zarith::encoding(), + ]))), + ), + ], + ) + } +} + +impl Display for DalAttestedSlots { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "DalAttestedSlots {{published_level: {}, number_of_slots: {}, slot_size: {}, page_size: {}, slots_by_publisher: {} entries}}", + self.published_level, + self.number_of_slots, + self.slot_size, + self.page_size, + self.slots_by_publisher.len() + ) + } +} + +/// Internal inbox message - known to be sent by the protocol. #[derive(Debug, PartialEq, Eq, NomReader, HasEncoding, BinWriter)] pub enum InternalInboxMessage { - /// Transfer message + /// Transfer message (tag 0) + #[encoding(tag = 0)] Transfer(Transfer), - /// Start of level message, pushed at the beginning of an inbox level. + /// Start of level message, pushed at the beginning of an inbox level (tag 1) + #[encoding(tag = 1)] StartOfLevel, - /// End of level message, pushed at the end of an inbox level. + /// End of level message, pushed at the end of an inbox level (tag 2) + #[encoding(tag = 2)] EndOfLevel, - /// Info per level, goes after StartOfLevel + /// Info per level, goes after StartOfLevel (tag 3) + #[encoding(tag = 3)] InfoPerLevel(InfoPerLevel), + /// Protocol migration message (tag 4) - contains the new protocol name + #[encoding(tag = 4)] + ProtocolMigration(String), + /// DAL attested slots message (tag 5) - contains attested slot info per publisher + #[encoding(tag = 5)] + DalAttestedSlots(DalAttestedSlots), } impl Display for InternalInboxMessage { @@ -144,6 +291,10 @@ impl Display for InternalInboxMessage { Self::StartOfLevel => write!(f, "StartOfLevel"), Self::EndOfLevel => write!(f, "EndOfLevel"), Self::InfoPerLevel(ipl) => write!(f, "{}", ipl), + Self::ProtocolMigration(proto) => { + write!(f, "ProtocolMigration {{protocol: {}}}", proto) + } + Self::DalAttestedSlots(dal) => write!(f, "{}", dal), } } } @@ -232,13 +383,19 @@ impl> BinWriter for ExternalMessageFrame { #[cfg(test)] mod test { + use super::DalAttestedSlots; use super::ExternalMessageFrame; use super::InboxMessage; use super::InternalInboxMessage; use crate::michelson::Michelson; use crate::michelson::MichelsonUnit; + use crate::public_key_hash::PublicKeyHash; use crate::smart_rollup::SmartRollupAddress; + use num_bigint::BigInt; + use std::collections::BTreeMap; use tezos_data_encoding::enc::BinWriter; + use tezos_data_encoding::nom::NomReader; + use tezos_data_encoding::types::Zarith; #[test] fn test_encode_decode_sol() { @@ -268,6 +425,15 @@ mod test { test_encode_decode::(expected_bytes, inbox_message) } + #[test] + fn test_encode_decode_protocol_migration() { + let inbox_message: InboxMessage = InboxMessage::Internal( + InternalInboxMessage::ProtocolMigration("PtAlphaProtocol".to_string()), + ); + + assert_encode_decode_inbox_message(inbox_message); + } + #[test] fn test_encode_decode_external_inbox_message() { let assert_enc = |message: Vec| { @@ -339,4 +505,86 @@ mod test { assert_eq!(framed, parsed); } + + /// Helper to create a bitset from a list of slot indices. + /// Sets bit i for each index i in the list. + fn make_bitset(indices: &[u32]) -> Zarith { + let mut value = BigInt::from(0); + for &idx in indices { + value |= BigInt::from(1) << idx; + } + Zarith(value) + } + + #[test] + fn test_encode_decode_dal_attested_slots_empty() { + let dal_attested = DalAttestedSlots { + published_level: 100, + number_of_slots: 16, + slot_size: 126944, + page_size: 4096, + slots_by_publisher: BTreeMap::new(), + }; + + let inbox_message: InboxMessage = + InboxMessage::Internal(InternalInboxMessage::DalAttestedSlots(dal_attested)); + + assert_encode_decode_inbox_message(inbox_message); + } + + #[test] + fn test_encode_decode_dal_attested_slots_with_slots_by_publisher() { + let pkh = PublicKeyHash::from_b58check("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx") + .expect("valid pkh"); + + let mut slots_by_publisher = BTreeMap::new(); + // Slots 0, 1, 2 attested -> bitset = 0b111 = 7 + slots_by_publisher.insert(pkh, make_bitset(&[0, 1, 2])); + + let dal_attested = DalAttestedSlots { + published_level: 42, + number_of_slots: 16, + slot_size: 126944, + page_size: 4096, + slots_by_publisher, + }; + + let inbox_message: InboxMessage = + InboxMessage::Internal(InternalInboxMessage::DalAttestedSlots(dal_attested)); + + assert_encode_decode_inbox_message(inbox_message); + } + + #[test] + fn test_dal_attested_slots_roundtrip() { + let pkh1 = PublicKeyHash::from_b58check("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx") + .expect("valid pkh"); + let pkh2 = PublicKeyHash::from_b58check("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN") + .expect("valid pkh"); + + let mut slots_by_publisher = BTreeMap::new(); + // Slots 0, 5, 10 attested -> bitset = 2^0 + 2^5 + 2^10 = 1 + 32 + 1024 = 1057 + slots_by_publisher.insert(pkh1, make_bitset(&[0, 5, 10])); + // Slots 1, 2 attested -> bitset = 2^1 + 2^2 = 2 + 4 = 6 + slots_by_publisher.insert(pkh2, make_bitset(&[1, 2])); + + let original = DalAttestedSlots { + published_level: 12345, + number_of_slots: 16, + slot_size: 126944, + page_size: 4096, + slots_by_publisher, + }; + + let mut encoded = Vec::new(); + original + .bin_write(&mut encoded) + .expect("encoding should work"); + + let (remaining, decoded) = + DalAttestedSlots::nom_read(&encoded).expect("decoding should work"); + + assert!(remaining.is_empty(), "all bytes should be consumed"); + assert_eq!(original, decoded); + } } diff --git a/src/kernel_tx_demo/kernel/src/lib.rs b/src/kernel_tx_demo/kernel/src/lib.rs index ab83f3ef823cb51b21801cda83e1e4293f59a952..497ac92838c1ab1bcd4c73c6072feabcca74b7d8 100644 --- a/src/kernel_tx_demo/kernel/src/lib.rs +++ b/src/kernel_tx_demo/kernel/src/lib.rs @@ -255,7 +255,10 @@ fn filter_inbox_message<'a, Host: Runtime>( } InboxMessage::Internal( - _msg @ (InternalInboxMessage::EndOfLevel | InternalInboxMessage::InfoPerLevel(..)), + _msg @ (InternalInboxMessage::EndOfLevel + | InternalInboxMessage::InfoPerLevel(..) + | InternalInboxMessage::ProtocolMigration(..) + | InternalInboxMessage::DalAttestedSlots(..)), ) => { #[cfg(feature = "debug")] debug_msg!(host, "InboxMetadata: {}\n", _msg);