storm/
container.rs

1// Storm Core library: distributed storage & messaging for lightning network.
2//
3// Written in 2022 by
4//     Dr. Maxim Orlovsky <orlovsky@lnp-bp.org>
5//
6// Copyright (C) 2022 by LNP/BP Standards Association, Switzerland.
7//
8// You should have received a copy of the MIT License along with this software.
9// If not, see <https://opensource.org/licenses/MIT>.
10
11use std::str::FromStr;
12
13use bitcoin_hashes::{sha256, sha256t};
14use commit_verify::{
15    commit_encode, CommitVerify, ConsensusCommit, PrehashedProtocol, TaggedHash,
16};
17use lnpbp_bech32::{FromBech32Str, ToBech32String};
18use stens::AsciiString;
19use strict_encoding::{MediumVec, StrictEncode};
20
21use crate::{ChunkId, MesgId};
22
23// "storm:container"
24static MIDSTATE_CONTAINER_ID: [u8; 32] = [
25    213, 126, 121, 18, 141, 34, 64, 215, 78, 163, 37, 117, 217, 166, 67, 226,
26    137, 2, 113, 215, 156, 29, 193, 183, 154, 55, 48, 250, 208, 149, 206, 14,
27];
28
29pub const STORM_CONTAINER_ID_HRP: &str = "storm";
30
31/// Tag used for [`ContainerId`] hash type
32pub struct ContainerIdTag;
33
34impl sha256t::Tag for ContainerIdTag {
35    #[inline]
36    fn engine() -> sha256::HashEngine {
37        let midstate = sha256::Midstate::from_inner(MIDSTATE_CONTAINER_ID);
38        sha256::HashEngine::from_midstate(midstate, 64)
39    }
40}
41
42/// Unique data container identifier
43#[derive(
44    Wrapper, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default,
45    Display, From
46)]
47#[derive(StrictEncode, StrictDecode)]
48#[wrapper(Debug, BorrowSlice)]
49#[display(ContainerId::to_bech32_string)]
50pub struct ContainerId(sha256t::Hash<ContainerIdTag>);
51
52impl<Msg> CommitVerify<Msg, PrehashedProtocol> for ContainerId
53where Msg: AsRef<[u8]>
54{
55    #[inline]
56    fn commit(msg: &Msg) -> ContainerId { ContainerId::hash(msg) }
57}
58
59impl commit_encode::Strategy for ContainerId {
60    type Strategy = commit_encode::strategies::UsingStrict;
61}
62
63impl lnpbp_bech32::Strategy for ContainerId {
64    const HRP: &'static str = STORM_CONTAINER_ID_HRP;
65    type Strategy = lnpbp_bech32::strategies::UsingStrictEncoding;
66}
67
68// TODO: Make this part of `lnpbp::bech32`
69#[cfg(feature = "serde")]
70impl serde::Serialize for ContainerId {
71    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
72    where S: serde::Serializer {
73        if serializer.is_human_readable() {
74            serializer.serialize_str(&self.to_bech32_string())
75        } else {
76            serializer.serialize_bytes(&self[..])
77        }
78    }
79}
80
81#[cfg(feature = "serde")]
82impl<'de> serde::Deserialize<'de> for ContainerId {
83    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
84    where D: serde::Deserializer<'de> {
85        struct Visitor;
86        impl serde::de::Visitor<'_> for Visitor {
87            type Value = ContainerId;
88
89            fn expecting(
90                &self,
91                formatter: &mut std::fmt::Formatter,
92            ) -> std::fmt::Result {
93                write!(
94                    formatter,
95                    "Bech32 string with `{}` HRP",
96                    STORM_CONTAINER_ID_HRP
97                )
98            }
99
100            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
101            where E: serde::de::Error {
102                ContainerId::from_str(v).map_err(serde::de::Error::custom)
103            }
104
105            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
106            where E: serde::de::Error {
107                self.visit_str(&v)
108            }
109
110            fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
111            where E: serde::de::Error {
112                ContainerId::from_bytes(&v).map_err(|_| {
113                    serde::de::Error::invalid_length(v.len(), &"32 bytes")
114                })
115            }
116        }
117
118        if deserializer.is_human_readable() {
119            deserializer.deserialize_str(Visitor)
120        } else {
121            deserializer.deserialize_byte_buf(Visitor)
122        }
123    }
124}
125
126impl FromStr for ContainerId {
127    type Err = lnpbp_bech32::Error;
128
129    fn from_str(s: &str) -> Result<Self, Self::Err> {
130        ContainerId::from_bech32_str(s)
131    }
132}
133
134#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Display)]
135#[derive(StrictEncode, StrictDecode)]
136#[cfg_attr(
137    feature = "serde",
138    derive(Serialize, Deserialize),
139    serde(crate = "serde_crate")
140)]
141#[display("{container_id}@{message_id}")]
142pub struct ContainerFullId {
143    /// Message defining access rights to the container.
144    pub message_id: MesgId,
145    pub container_id: ContainerId,
146}
147
148#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash, AsAny)]
149#[derive(StrictEncode, StrictDecode)]
150#[cfg_attr(
151    feature = "serde",
152    derive(Serialize, Deserialize),
153    serde(crate = "serde_crate")
154)]
155pub struct ContainerHeader {
156    /// Version of the container. Always 0 for now.
157    pub version: u16,
158    /// MIME type of the file.
159    pub mime: AsciiString, // TODO: Create a dedicated MIME type
160    /// UTF-8 description of the file.
161    pub info: String,
162    /// Container size, which is the sum of sizes of the individual chunks.
163    ///
164    /// Consensus limitation of the container size is 43 bits: 19 bits for the
165    /// number of chunks and up to 24 bits for chunk size. 19 bits for the max
166    /// number of chunks comes from the fact that the total size of the
167    /// container index must be below 2^24 bytes (to fit into a LN packet);
168    /// since the size of the chunk id is 2^5 (32) bits, and the maximum
169    /// Bifrost packet size is 2^24, we have only 24-5=19 bits to store the
170    /// chunk index.
171    pub size: u64,
172}
173
174// TODO: Add convenience constructors for ContainerHeader constructing from
175//       a given mime type const string
176
177#[derive(Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Debug, Display)]
178#[derive(NetworkEncode, NetworkDecode)]
179#[display("{id}")]
180pub struct ContainerInfo {
181    pub header: ContainerHeader,
182    pub id: ContainerFullId,
183}
184
185#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash, AsAny)]
186#[derive(StrictEncode, StrictDecode)]
187#[cfg_attr(
188    feature = "serde",
189    derive(Serialize, Deserialize),
190    serde(crate = "serde_crate")
191)]
192pub struct Container {
193    pub header: ContainerHeader,
194    pub chunks: MediumVec<ChunkId>,
195}
196
197impl commit_encode::Strategy for Container {
198    type Strategy = commit_encode::strategies::UsingStrict;
199}
200
201impl ConsensusCommit for Container {
202    type Commitment = ContainerId;
203}
204
205impl Container {
206    pub fn container_id(&self) -> ContainerId { self.consensus_commit() }
207}
208
209#[cfg(test)]
210mod test {
211    use amplify::Wrapper;
212    use commit_verify::tagged_hash;
213
214    use super::*;
215
216    #[test]
217    fn test_container_id_midstate() {
218        let midstate = tagged_hash::Midstate::with(b"storm:container");
219        assert_eq!(midstate.into_inner().into_inner(), MIDSTATE_CONTAINER_ID);
220    }
221}