Skip to main content

river_core/room_state/
version.rs

1//! State version tracking for contract migrations.
2//!
3//! The version field allows contracts to:
4//! 1. Detect states from older contract versions
5//! 2. Reject states from unknown future versions
6//! 3. Perform migration logic if needed
7
8use super::{ChatRoomParametersV1, ChatRoomStateV1};
9use freenet_scaffold::ComposableState;
10use serde::{Deserialize, Serialize};
11
12/// Current state version. Increment when making breaking changes to state format.
13pub const CURRENT_STATE_VERSION: u32 = 1;
14
15/// Wrapper for state version that implements ComposableState.
16///
17/// The version is metadata about the state format, not user content.
18/// It doesn't change via deltas - it's set when the state is created
19/// and verified to be compatible when loaded.
20#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Debug)]
21pub struct StateVersion(pub u32);
22
23impl ComposableState for StateVersion {
24    type ParentState = ChatRoomStateV1;
25    type Summary = u32;
26    type Delta = ();
27    type Parameters = ChatRoomParametersV1;
28
29    fn verify(
30        &self,
31        _parent_state: &Self::ParentState,
32        _parameters: &Self::Parameters,
33    ) -> Result<(), String> {
34        // Accept version 0 (legacy states without version) and current version
35        // Reject unknown future versions
36        if self.0 > CURRENT_STATE_VERSION {
37            return Err(format!(
38                "Unknown state version {}. This contract supports versions 0-{}. \
39                 Please upgrade your client.",
40                self.0, CURRENT_STATE_VERSION
41            ));
42        }
43        Ok(())
44    }
45
46    fn summarize(
47        &self,
48        _parent_state: &Self::ParentState,
49        _parameters: &Self::Parameters,
50    ) -> Self::Summary {
51        self.0
52    }
53
54    fn delta(
55        &self,
56        _parent_state: &Self::ParentState,
57        _parameters: &Self::Parameters,
58        _old_state_summary: &Self::Summary,
59    ) -> Option<Self::Delta> {
60        // Version never changes via delta
61        None
62    }
63
64    fn apply_delta(
65        &mut self,
66        _parent_state: &Self::ParentState,
67        _parameters: &Self::Parameters,
68        _delta: &Option<Self::Delta>,
69    ) -> Result<(), String> {
70        // Version doesn't change via delta
71        // When migrating, the contract would set this directly
72        Ok(())
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use freenet_stdlib::prelude::serde_json;
80
81    #[test]
82    fn test_version_default_is_zero() {
83        let v = StateVersion::default();
84        assert_eq!(v.0, 0);
85    }
86
87    #[test]
88    fn test_version_verify_accepts_current() {
89        let v = StateVersion(CURRENT_STATE_VERSION);
90        let parent = ChatRoomStateV1::default();
91        let params = ChatRoomParametersV1 {
92            owner: ed25519_dalek::SigningKey::generate(&mut rand::rngs::OsRng).verifying_key(),
93        };
94        assert!(v.verify(&parent, &params).is_ok());
95    }
96
97    #[test]
98    fn test_version_verify_accepts_legacy() {
99        let v = StateVersion(0);
100        let parent = ChatRoomStateV1::default();
101        let params = ChatRoomParametersV1 {
102            owner: ed25519_dalek::SigningKey::generate(&mut rand::rngs::OsRng).verifying_key(),
103        };
104        assert!(v.verify(&parent, &params).is_ok());
105    }
106
107    #[test]
108    fn test_version_verify_rejects_future() {
109        let v = StateVersion(CURRENT_STATE_VERSION + 1);
110        let parent = ChatRoomStateV1::default();
111        let params = ChatRoomParametersV1 {
112            owner: ed25519_dalek::SigningKey::generate(&mut rand::rngs::OsRng).verifying_key(),
113        };
114        assert!(v.verify(&parent, &params).is_err());
115    }
116
117    #[test]
118    fn test_version_serialization_roundtrip() {
119        // Test that version field serializes and deserializes correctly
120        let v = StateVersion(CURRENT_STATE_VERSION);
121        let serialized = serde_json::to_string(&v).unwrap();
122        let deserialized: StateVersion = serde_json::from_str(&serialized).unwrap();
123        assert_eq!(v, deserialized);
124    }
125
126    #[test]
127    fn test_state_without_version_field_deserializes_with_default() {
128        // Simulate a legacy state JSON that doesn't have a version field
129        // The #[serde(default)] attribute should make version = 0
130        use crate::room_state::configuration::{AuthorizedConfigurationV1, Configuration};
131
132        let owner_sk = ed25519_dalek::SigningKey::generate(&mut rand::rngs::OsRng);
133        let owner_vk = owner_sk.verifying_key();
134
135        // Create a minimal state and serialize it
136        let mut state = ChatRoomStateV1::default();
137        let config = Configuration {
138            owner_member_id: owner_vk.into(),
139            ..Default::default()
140        };
141        state.configuration = AuthorizedConfigurationV1::new(config, &owner_sk);
142
143        // Serialize to JSON, then manually remove the version field to simulate legacy state
144        let mut json_value: serde_json::Value = serde_json::to_value(&state).unwrap();
145        if let serde_json::Value::Object(ref mut map) = json_value {
146            map.remove("version");
147        }
148        let legacy_json = serde_json::to_string(&json_value).unwrap();
149
150        // Deserialize - should use default version (0)
151        let deserialized: ChatRoomStateV1 = serde_json::from_str(&legacy_json).unwrap();
152        assert_eq!(
153            deserialized.version.0, 0,
154            "Legacy state without version field should deserialize with version=0"
155        );
156    }
157
158    #[test]
159    fn test_state_with_version_field_roundtrips() {
160        use crate::room_state::configuration::{AuthorizedConfigurationV1, Configuration};
161
162        let owner_sk = ed25519_dalek::SigningKey::generate(&mut rand::rngs::OsRng);
163        let owner_vk = owner_sk.verifying_key();
164
165        let mut state = ChatRoomStateV1::default();
166        let config = Configuration {
167            owner_member_id: owner_vk.into(),
168            ..Default::default()
169        };
170        state.configuration = AuthorizedConfigurationV1::new(config, &owner_sk);
171        state.version = StateVersion(CURRENT_STATE_VERSION);
172
173        // Serialize and deserialize
174        let json = serde_json::to_string(&state).unwrap();
175        let deserialized: ChatRoomStateV1 = serde_json::from_str(&json).unwrap();
176
177        assert_eq!(deserialized.version.0, CURRENT_STATE_VERSION);
178    }
179}