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