Skip to main content

triblespace_core/repo/
commit.rs

1use crate::macros::entity;
2use crate::macros::pattern;
3use crate::inline::TryToInline;
4use ed25519::Signature;
5use ed25519_dalek::SignatureError;
6use ed25519_dalek::SigningKey;
7use ed25519_dalek::Verifier;
8use ed25519_dalek::VerifyingKey;
9use itertools::Itertools;
10
11use ed25519::signature::Signer;
12
13use crate::blob::encodings::longstring::LongString;
14use crate::blob::encodings::simplearchive::SimpleArchive;
15use crate::blob::Blob;
16use crate::prelude::inlineencodings::Handle;
17use crate::query::find;
18use crate::trible::TribleSet;
19use crate::inline::Inline;
20
21
22/// Error returned when commit signature verification fails.
23pub enum ValidationError {
24    /// The metadata contains multiple signature entities for the same commit.
25    AmbiguousSignature,
26    /// No signature information was found in the metadata.
27    MissingSignature,
28    /// The signature did not match the commit bytes or the public key was invalid.
29    FailedValidation,
30}
31
32impl From<SignatureError> for ValidationError {
33    /// Converts an Ed25519 signature error into a [`ValidationError::FailedValidation`].
34    fn from(_: SignatureError) -> Self {
35        ValidationError::FailedValidation
36    }
37}
38
39/// Constructs commit metadata describing `content`, optional `metadata`, and its parent commits.
40///
41/// The resulting [`TribleSet`] is signed using `signing_key` when content is
42/// present, so that its authenticity can later be verified. If `msg` is
43/// provided it is stored as a long commit message via a LongString blob
44/// handle. If `metadata` is provided it is stored as a SimpleArchive handle.
45///
46/// The commit's entity id is derived intrinsically from the
47/// `(attribute, value)` pairs present in the metadata — so two commits with
48/// identical content, parents, and signatures collide on entity id and blob
49/// hash alike. This matters especially for **merge commits**
50/// (`content = None`): merges carry no author-specific bits (no signature,
51/// no timestamp, no random entity id), so two peers merging the same parent
52/// set produce bit-identical merge commits, and parallel-merge scenarios
53/// converge in zero extra rounds.
54pub fn commit_metadata(
55    signing_key: &SigningKey,
56    parents: impl IntoIterator<Item = Inline<Handle<SimpleArchive>>>,
57    msg: Option<Inline<Handle<LongString>>>,
58    content: Option<Blob<SimpleArchive>>,
59    metadata: Option<Inline<Handle<SimpleArchive>>>,
60) -> TribleSet {
61    // Authored commits carry a timestamp and a signature. Merge commits
62    // (content = None) carry neither, so they stay content-deterministic.
63    let (content_handle, signed_by, signature, created_at) = match content.as_ref() {
64        Some(blob) => {
65            // Through the clock seam (not Epoch::now directly) so
66            // simulated executions mint deterministic, virtual-time
67            // commit timestamps — bit-identical commits per seed.
68            let now = crate::clock::epoch_now();
69            let timestamp: Inline<_> =
70                (now, now).try_to_inline().expect("point interval");
71            (
72                Some(blob.get_handle()),
73                Some(signing_key.verifying_key()),
74                Some(signing_key.sign(&blob.bytes)),
75                Some(timestamp),
76            )
77        }
78        None => (None, None, None, None),
79    };
80    let parents: Vec<_> = parents.into_iter().collect();
81
82    // `entity!` without an explicit `id @` prefix derives the entity id
83    // by hashing the sorted/deduped (attr_id, value) pairs. The resulting
84    // commit is content-addressed at both the blob level (via
85    // SimpleArchive) and the entity-id level.
86    let fragment = entity! {
87        crate::metadata::created_at?: created_at,
88        super::content?: content_handle,
89        super::signed_by?: signed_by,
90        super::signature_r?: signature,
91        super::signature_s?: signature,
92        super::message?: msg,
93        super::metadata?: metadata,
94        super::parent*: parents,
95    };
96
97    fragment.into()
98}
99
100/// Validates that the `metadata` blob genuinely signs the supplied commit
101/// `content`.
102///
103/// Returns an error if the signature information is missing, malformed or does
104/// not match the commit bytes.
105pub fn verify(content: Blob<SimpleArchive>, metadata: TribleSet) -> Result<(), ValidationError> {
106    let handle = content.get_handle();
107    let (pubkey, r, s) = match find!(
108    (pubkey: Inline<_>, r, s),
109    pattern!(&metadata, [
110    {
111        super::content: handle,
112        super::signed_by: ?pubkey,
113        super::signature_r: ?r,
114        super::signature_s: ?s
115    }]))
116    .at_most_one()
117    {
118        Ok(Some(result)) => result,
119        Ok(None) => return Err(ValidationError::MissingSignature),
120        Err(_) => return Err(ValidationError::AmbiguousSignature),
121    };
122
123    let pubkey: VerifyingKey = pubkey.try_from_inline()?;
124    let signature = Signature::from_components(r, s);
125    pubkey.verify(&content.bytes, &signature)?;
126    Ok(())
127}