1use crate::base::canonical::canonical_json;
4use crate::base::hash::{sha256, Hash256};
5use crate::base::time::Time;
6use crate::record::author::Author;
7use crate::record::evidence::Evidence;
8use crate::record::kind::Kind;
9use crate::record::refs::{RecordId, Ref};
10use serde::{Deserialize, Serialize};
11
12pub type SpaceId = Hash256;
15pub type ThreadId = Hash256;
18pub type ScopeId = Hash256;
21
22pub const RECORD_SIGNATURE_DOMAIN: &str = "bellbook.record-signature.v0.2";
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(deny_unknown_fields)]
33pub struct Record {
34 pub id: RecordId,
38 pub space: SpaceId,
40 pub thread: ThreadId,
42 pub time: Time,
44 pub author: Author,
46 pub kind: Kind,
48 pub schema: Hash256,
50 pub data: Vec<u8>,
52 pub refs: Vec<Ref>,
55 pub evidence: Evidence,
58}
59
60impl Record {
61 pub fn with_computed_id(mut self) -> Result<Self, serde_json::Error> {
67 self.id = self.compute_id()?;
68 Ok(self)
69 }
70
71 pub fn compute_id(&self) -> Result<RecordId, serde_json::Error> {
73 Ok(sha256(&self.canonical_id_form()?))
74 }
75
76 pub fn canonical_id_form(&self) -> Result<Vec<u8>, serde_json::Error> {
79 let id_form = CanonicalIdForm {
80 space: &self.space,
81 thread: &self.thread,
82 time: self.time,
83 author: CanonicalIdAuthor {
84 id: &self.author.id,
85 type_: &self.author.type_,
86 signature: self.author.signature.as_ref(),
87 },
88 kind: &self.kind,
89 schema: &self.schema,
90 data: &self.data,
91 refs: &self.refs,
92 evidence: &self.evidence,
93 };
94 canonical_json(&id_form)
95 }
96
97 pub fn signing_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
104 let signing_form = DomainSeparatedSigningForm {
105 domain: RECORD_SIGNATURE_DOMAIN,
106 record: CanonicalSigningForm {
107 space: &self.space,
108 thread: &self.thread,
109 time: self.time,
110 author: CanonicalAuthor {
111 id: &self.author.id,
112 type_: &self.author.type_,
113 },
114 kind: &self.kind,
115 schema: &self.schema,
116 data: &self.data,
117 refs: &self.refs,
118 evidence: &self.evidence,
119 },
120 };
121 canonical_json(&signing_form)
122 }
123}
124
125#[derive(Serialize)]
129struct DomainSeparatedSigningForm<'a> {
130 domain: &'static str,
131 record: CanonicalSigningForm<'a>,
132}
133
134#[derive(Serialize)]
136struct CanonicalIdForm<'a> {
137 space: &'a SpaceId,
138 thread: &'a ThreadId,
139 time: Time,
140 author: CanonicalIdAuthor<'a>,
141 kind: &'a Kind,
142 schema: &'a Hash256,
143 data: &'a Vec<u8>,
144 refs: &'a Vec<Ref>,
145 evidence: &'a Evidence,
146}
147
148#[derive(Serialize)]
149struct CanonicalIdAuthor<'a> {
150 id: &'a str,
151 #[serde(rename = "type")]
152 type_: &'a crate::record::kind::AuthorType,
153 #[serde(skip_serializing_if = "Option::is_none")]
154 signature: Option<&'a crate::record::author::Signature>,
155}
156
157#[derive(Serialize)]
159struct CanonicalSigningForm<'a> {
160 space: &'a SpaceId,
161 thread: &'a ThreadId,
162 time: Time,
163 author: CanonicalAuthor<'a>,
164 kind: &'a Kind,
165 schema: &'a Hash256,
166 data: &'a Vec<u8>,
167 refs: &'a Vec<Ref>,
168 evidence: &'a Evidence,
169}
170
171#[derive(Serialize)]
172struct CanonicalAuthor<'a> {
173 id: &'a str,
174 #[serde(rename = "type")]
175 type_: &'a crate::record::kind::AuthorType,
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181#[serde(deny_unknown_fields)]
182pub struct Proposal {
183 pub space: SpaceId,
185 pub thread: ThreadId,
187 pub author: Author,
189 pub kind: Kind,
191 pub schema: Hash256,
193 pub data: Vec<u8>,
195 pub refs: Vec<Ref>,
198}
199
200pub fn decode<T: serde::de::DeserializeOwned>(data: &[u8]) -> Result<T, serde_json::Error> {
202 serde_json::from_slice(data)
203}
204
205pub fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>, serde_json::Error> {
207 canonical_json(value)
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use crate::record::kind::*;
214
215 fn test_record() -> Record {
216 Record {
217 id: [0u8; 32],
218 space: [1u8; 32],
219 thread: [2u8; 32],
220 time: 1,
221 author: Author {
222 id: "test".into(),
223 type_: AuthorType::User,
224 signature: None,
225 },
226 kind: Kind::Request,
227 schema: [3u8; 32],
228 data: b"{}".to_vec(),
229 refs: vec![],
230 evidence: Evidence::Reported,
231 }
232 }
233
234 #[test]
235 fn test_with_computed_id() {
236 let record = test_record().with_computed_id().unwrap();
237 assert_ne!(record.id, [0u8; 32]);
239 let id2 = record.compute_id().unwrap();
241 assert_eq!(record.id, id2);
242 }
243
244 #[test]
245 fn test_id_form_excludes_id_but_binds_signature() {
246 let mut r1 = test_record();
247 r1.id = [99u8; 32];
248 let mut r2 = r1.clone();
249 r2.id = [88u8; 32];
250 assert_eq!(
251 r1.canonical_id_form().unwrap(),
252 r2.canonical_id_form().unwrap()
253 );
254
255 r2.author.signature = Some(crate::record::author::Signature {
256 key_id: "11".repeat(32),
257 sig: vec![1; 64],
258 });
259 assert_ne!(
260 r1.canonical_id_form().unwrap(),
261 r2.canonical_id_form().unwrap()
262 );
263 assert_ne!(r1.compute_id().unwrap(), r2.compute_id().unwrap());
264 }
265
266 #[test]
267 fn test_signing_bytes_exclude_id_and_signature() {
268 let mut r1 = test_record();
269 r1.id = [99u8; 32];
270 r1.author.signature = Some(crate::record::author::Signature {
271 key_id: "11".repeat(32),
272 sig: vec![1; 64],
273 });
274
275 let mut r2 = test_record();
276 r2.id = [88u8; 32];
277 r2.author.signature = None;
278
279 assert_eq!(r1.signing_bytes().unwrap(), r2.signing_bytes().unwrap());
280
281 let value: serde_json::Value =
282 serde_json::from_slice(&r1.signing_bytes().unwrap()).unwrap();
283 assert_eq!(value["domain"], RECORD_SIGNATURE_DOMAIN);
284 assert!(value.get("record").is_some());
285 }
286
287 #[test]
288 fn test_id_determinism() {
289 let r1 = test_record().with_computed_id().unwrap();
290 let r2 = test_record().with_computed_id().unwrap();
291 assert_eq!(r1.id, r2.id);
292 }
293}