use crate::base::canonical::canonical_json;
use crate::base::hash::{sha256, Hash256};
use crate::base::time::Time;
use crate::record::author::Author;
use crate::record::evidence::Evidence;
use crate::record::kind::Kind;
use crate::record::refs::{RecordId, Ref};
use serde::{Deserialize, Serialize};
pub type SpaceId = Hash256;
pub type ThreadId = Hash256;
pub type ScopeId = Hash256;
pub const RECORD_SIGNATURE_DOMAIN: &str = "bellbook.record-signature.v0.2";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Record {
pub id: RecordId,
pub space: SpaceId,
pub thread: ThreadId,
pub time: Time,
pub author: Author,
pub kind: Kind,
pub schema: Hash256,
pub data: Vec<u8>,
pub refs: Vec<Ref>,
pub evidence: Evidence,
}
impl Record {
pub fn with_computed_id(mut self) -> Result<Self, serde_json::Error> {
self.id = self.compute_id()?;
Ok(self)
}
pub fn compute_id(&self) -> Result<RecordId, serde_json::Error> {
Ok(sha256(&self.canonical_id_form()?))
}
pub fn canonical_id_form(&self) -> Result<Vec<u8>, serde_json::Error> {
let id_form = CanonicalIdForm {
space: &self.space,
thread: &self.thread,
time: self.time,
author: CanonicalIdAuthor {
id: &self.author.id,
type_: &self.author.type_,
signature: self.author.signature.as_ref(),
},
kind: &self.kind,
schema: &self.schema,
data: &self.data,
refs: &self.refs,
evidence: &self.evidence,
};
canonical_json(&id_form)
}
pub fn signing_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
let signing_form = DomainSeparatedSigningForm {
domain: RECORD_SIGNATURE_DOMAIN,
record: CanonicalSigningForm {
space: &self.space,
thread: &self.thread,
time: self.time,
author: CanonicalAuthor {
id: &self.author.id,
type_: &self.author.type_,
},
kind: &self.kind,
schema: &self.schema,
data: &self.data,
refs: &self.refs,
evidence: &self.evidence,
},
};
canonical_json(&signing_form)
}
}
#[derive(Serialize)]
struct DomainSeparatedSigningForm<'a> {
domain: &'static str,
record: CanonicalSigningForm<'a>,
}
#[derive(Serialize)]
struct CanonicalIdForm<'a> {
space: &'a SpaceId,
thread: &'a ThreadId,
time: Time,
author: CanonicalIdAuthor<'a>,
kind: &'a Kind,
schema: &'a Hash256,
data: &'a Vec<u8>,
refs: &'a Vec<Ref>,
evidence: &'a Evidence,
}
#[derive(Serialize)]
struct CanonicalIdAuthor<'a> {
id: &'a str,
#[serde(rename = "type")]
type_: &'a crate::record::kind::AuthorType,
#[serde(skip_serializing_if = "Option::is_none")]
signature: Option<&'a crate::record::author::Signature>,
}
#[derive(Serialize)]
struct CanonicalSigningForm<'a> {
space: &'a SpaceId,
thread: &'a ThreadId,
time: Time,
author: CanonicalAuthor<'a>,
kind: &'a Kind,
schema: &'a Hash256,
data: &'a Vec<u8>,
refs: &'a Vec<Ref>,
evidence: &'a Evidence,
}
#[derive(Serialize)]
struct CanonicalAuthor<'a> {
id: &'a str,
#[serde(rename = "type")]
type_: &'a crate::record::kind::AuthorType,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Proposal {
pub space: SpaceId,
pub thread: ThreadId,
pub author: Author,
pub kind: Kind,
pub schema: Hash256,
pub data: Vec<u8>,
pub refs: Vec<Ref>,
}
pub fn decode<T: serde::de::DeserializeOwned>(data: &[u8]) -> Result<T, serde_json::Error> {
serde_json::from_slice(data)
}
pub fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>, serde_json::Error> {
canonical_json(value)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::kind::*;
fn test_record() -> Record {
Record {
id: [0u8; 32],
space: [1u8; 32],
thread: [2u8; 32],
time: 1,
author: Author {
id: "test".into(),
type_: AuthorType::User,
signature: None,
},
kind: Kind::Request,
schema: [3u8; 32],
data: b"{}".to_vec(),
refs: vec![],
evidence: Evidence::Reported,
}
}
#[test]
fn test_with_computed_id() {
let record = test_record().with_computed_id().unwrap();
assert_ne!(record.id, [0u8; 32]);
let id2 = record.compute_id().unwrap();
assert_eq!(record.id, id2);
}
#[test]
fn test_id_form_excludes_id_but_binds_signature() {
let mut r1 = test_record();
r1.id = [99u8; 32];
let mut r2 = r1.clone();
r2.id = [88u8; 32];
assert_eq!(
r1.canonical_id_form().unwrap(),
r2.canonical_id_form().unwrap()
);
r2.author.signature = Some(crate::record::author::Signature {
key_id: "11".repeat(32),
sig: vec![1; 64],
});
assert_ne!(
r1.canonical_id_form().unwrap(),
r2.canonical_id_form().unwrap()
);
assert_ne!(r1.compute_id().unwrap(), r2.compute_id().unwrap());
}
#[test]
fn test_signing_bytes_exclude_id_and_signature() {
let mut r1 = test_record();
r1.id = [99u8; 32];
r1.author.signature = Some(crate::record::author::Signature {
key_id: "11".repeat(32),
sig: vec![1; 64],
});
let mut r2 = test_record();
r2.id = [88u8; 32];
r2.author.signature = None;
assert_eq!(r1.signing_bytes().unwrap(), r2.signing_bytes().unwrap());
let value: serde_json::Value =
serde_json::from_slice(&r1.signing_bytes().unwrap()).unwrap();
assert_eq!(value["domain"], RECORD_SIGNATURE_DOMAIN);
assert!(value.get("record").is_some());
}
#[test]
fn test_id_determinism() {
let r1 = test_record().with_computed_id().unwrap();
let r2 = test_record().with_computed_id().unwrap();
assert_eq!(r1.id, r2.id);
}
}