Skip to main content

freenet_git_repo_contract/
lib.rs

1//! Repo contract: mutable repo state for freenet-git.
2//!
3//! This file is a thin shim: it deserializes the [`Parameters`] / [`State`] /
4//! delta payloads, dispatches into the pure-Rust logic in
5//! [`freenet_git_types`], and re-serializes results back into stdlib types.
6//! All security-relevant logic (signature verification, ACL gating,
7//! freshness rules, CRDT merge) lives in `freenet-git-types`, where it can
8//! be exhaustively unit-tested without booting a WASM runtime.
9//!
10//! Phase 1.0 is **single-writer**. The schema contains the full ACL fields
11//! ([`AclState`], [`WriterGrant`]) so that Phase 1.1's multi-writer ACL
12//! becomes a contract WASM upgrade rather than a fundamentally different
13//! schema. `validate_state` rejects any entry whose `updater` is not the
14//! repo owner.
15
16#![deny(unsafe_code)]
17
18use freenet_git_types as types;
19use freenet_git_types::{RepoParams, RepoState, RepoSummary};
20use freenet_stdlib::prelude::*;
21
22/// Marker type implementing [`ContractInterface`]. Re-exported so
23/// integration tests can dispatch through the same boundary the host
24/// runtime calls into.
25pub struct Contract;
26
27#[contract]
28impl ContractInterface for Contract {
29    fn validate_state(
30        parameters: Parameters<'static>,
31        state: State<'static>,
32        _related: RelatedContracts<'static>,
33    ) -> Result<ValidateResult, ContractError> {
34        let params = decode_params(&parameters)?;
35        let state = decode_state(&state)?;
36        match types::validate_state(&params, &state) {
37            Ok(()) => Ok(ValidateResult::Valid),
38            Err(e) => Err(ContractError::InvalidUpdateWithInfo {
39                reason: format!("validate_state: {e}"),
40            }),
41        }
42    }
43
44    fn update_state(
45        parameters: Parameters<'static>,
46        state: State<'static>,
47        data: Vec<UpdateData<'static>>,
48    ) -> Result<UpdateModification<'static>, ContractError> {
49        let params = decode_params(&parameters)?;
50        let mut current = decode_state(&state)?;
51
52        for update in data {
53            match update {
54                UpdateData::State(new_state_bytes) => {
55                    let incoming = decode_state(new_state_bytes.as_ref())?;
56                    current = types::update_state(&params, &current, &incoming).map_err(|e| {
57                        ContractError::InvalidUpdateWithInfo {
58                            reason: format!("update_state (full state): {e}"),
59                        }
60                    })?;
61                }
62                UpdateData::Delta(delta_bytes) => {
63                    if delta_bytes.as_ref().is_empty() {
64                        continue;
65                    }
66                    let delta = decode_state(delta_bytes.as_ref())?;
67                    current = types::update_state(&params, &current, &delta).map_err(|e| {
68                        ContractError::InvalidUpdateWithInfo {
69                            reason: format!("update_state (delta): {e}"),
70                        }
71                    })?;
72                }
73                UpdateData::StateAndDelta { state, delta } => {
74                    let incoming_state = decode_state(state.as_ref())?;
75                    current =
76                        types::update_state(&params, &current, &incoming_state).map_err(|e| {
77                            ContractError::InvalidUpdateWithInfo {
78                                reason: format!("update_state (state+delta state half): {e}"),
79                            }
80                        })?;
81                    if !delta.as_ref().is_empty() {
82                        let incoming_delta = decode_state(delta.as_ref())?;
83                        current = types::update_state(&params, &current, &incoming_delta).map_err(
84                            |e| ContractError::InvalidUpdateWithInfo {
85                                reason: format!("update_state (state+delta delta half): {e}"),
86                            },
87                        )?;
88                    }
89                }
90                UpdateData::RelatedState { .. }
91                | UpdateData::RelatedDelta { .. }
92                | UpdateData::RelatedStateAndDelta { .. } => {
93                    // Phase 1.0 has no cross-contract references in repo state.
94                    // Reject rather than silently accept so a future schema
95                    // that *does* use related state can land cleanly.
96                    return Err(ContractError::InvalidUpdate);
97                }
98                _ => {
99                    return Err(ContractError::InvalidUpdate);
100                }
101            }
102        }
103
104        let bytes =
105            bincode::serialize(&current).map_err(|e| ContractError::InvalidUpdateWithInfo {
106                reason: format!("re-serialize updated state: {e}"),
107            })?;
108        Ok(UpdateModification::valid(bytes.into()))
109    }
110
111    fn summarize_state(
112        parameters: Parameters<'static>,
113        state: State<'static>,
114    ) -> Result<StateSummary<'static>, ContractError> {
115        let _params = decode_params(&parameters)?;
116        let state = decode_state(&state)?;
117        let summary = types::summarize_state(&state);
118        let bytes =
119            bincode::serialize(&summary).map_err(|e| ContractError::InvalidUpdateWithInfo {
120                reason: format!("serialize summary: {e}"),
121            })?;
122        Ok(StateSummary::from(bytes))
123    }
124
125    fn get_state_delta(
126        parameters: Parameters<'static>,
127        state: State<'static>,
128        summary: StateSummary<'static>,
129    ) -> Result<StateDelta<'static>, ContractError> {
130        let _params = decode_params(&parameters)?;
131        let state = decode_state(&state)?;
132        let summary = decode_summary(&summary)?;
133        let delta = types::get_state_delta(&state, &summary);
134        // We use the same RepoState wire format for deltas. Empty delta
135        // means "you are caught up"; encode as empty bytes so the host can
136        // short-circuit without a full deserialize.
137        if is_empty_delta(&delta) {
138            return Ok(StateDelta::from(Vec::new()));
139        }
140        let bytes =
141            bincode::serialize(&delta).map_err(|e| ContractError::InvalidUpdateWithInfo {
142                reason: format!("serialize delta: {e}"),
143            })?;
144        Ok(StateDelta::from(bytes))
145    }
146}
147
148fn decode_params(p: &Parameters<'_>) -> Result<RepoParams, ContractError> {
149    RepoParams::from_bytes(p.as_ref()).map_err(|e| ContractError::InvalidUpdateWithInfo {
150        reason: format!("decode params: {e}"),
151    })
152}
153
154fn decode_state(bytes: &[u8]) -> Result<RepoState, ContractError> {
155    RepoState::from_bytes(bytes).map_err(|e| ContractError::InvalidUpdateWithInfo {
156        reason: format!("decode state: {e}"),
157    })
158}
159
160fn decode_summary(s: &StateSummary<'_>) -> Result<RepoSummary, ContractError> {
161    let bytes = s.as_ref();
162    if bytes.is_empty() {
163        return Ok(RepoSummary::default());
164    }
165    bincode::deserialize::<RepoSummary>(bytes).map_err(|e| ContractError::InvalidUpdateWithInfo {
166        reason: format!("decode summary: {e}"),
167    })
168}
169
170fn is_empty_delta(s: &RepoState) -> bool {
171    s.name.is_none()
172        && s.description.is_none()
173        && s.default_branch.is_none()
174        && s.force_push_allowed.is_none()
175        && s.acl.is_none()
176        && s.upgrade.is_none()
177        && s.refs.is_empty()
178        && s.object_index.is_empty()
179        && s.extensions.is_empty()
180    // We deliberately also accept the merged state output of
181    // `update_state` as a delta; the contract framework permits this.
182    // For internal use, we never construct merged states with all of
183    // these empty.
184}
185
186// Reference merge_state once so it's part of the contract's link surface
187// even if a future refactor stops calling it from update_state directly.
188// Cheap, doesn't run at runtime.
189#[allow(dead_code)]
190fn _force_link_merge_state() -> RepoState {
191    let a = RepoState::default();
192    let b = RepoState::default();
193    types::merge_state(&a, &b)
194}