guts_node/
consensus_app.rs

1//! Consensus application implementation.
2//!
3//! This module provides the implementation of the `ConsensusApplication` trait
4//! that applies finalized transactions to the node's state (repositories,
5//! collaboration, authentication, etc.).
6
7use async_trait::async_trait;
8use guts_auth::AuthStore;
9use guts_collaboration::CollaborationStore;
10use guts_consensus::{ConsensusApplication, ConsensusError, FinalizedBlock, Result, Transaction};
11use guts_realtime::{EventHub, EventKind};
12use guts_storage::RepoStore;
13use parking_lot::RwLock;
14use std::sync::Arc;
15use tracing::{debug, error, info, warn};
16
17/// The Guts application that applies consensus transactions to state.
18pub struct GutsApplication {
19    /// Repository store.
20    repos: Arc<RepoStore>,
21
22    /// Collaboration store (PRs, issues, comments).
23    /// Used when applying collaboration-related transactions (CreateIssue, CreatePullRequest, etc.)
24    #[allow(dead_code)]
25    collaboration: Arc<CollaborationStore>,
26
27    /// Auth store (orgs, teams, permissions).
28    /// Used when applying auth-related transactions (CreateOrganization, etc.)
29    #[allow(dead_code)]
30    auth: Arc<AuthStore>,
31
32    /// Real-time event hub for broadcasting updates.
33    realtime: Arc<EventHub>,
34
35    /// Current block height.
36    height: RwLock<u64>,
37
38    /// Current state root (hash of all state).
39    state_root: RwLock<[u8; 32]>,
40}
41
42impl GutsApplication {
43    /// Creates a new Guts application.
44    pub fn new(
45        repos: Arc<RepoStore>,
46        collaboration: Arc<CollaborationStore>,
47        auth: Arc<AuthStore>,
48        realtime: Arc<EventHub>,
49    ) -> Self {
50        Self {
51            repos,
52            collaboration,
53            auth,
54            realtime,
55            height: RwLock::new(0),
56            state_root: RwLock::new([0u8; 32]),
57        }
58    }
59
60    /// Applies a transaction to the state.
61    fn apply_transaction(&self, tx: &Transaction) -> Result<()> {
62        match tx {
63            Transaction::CreateRepository {
64                owner,
65                name,
66                description: _,
67                default_branch: _,
68                visibility: _,
69                creator: _,
70                signature: _,
71            } => {
72                // Create the repository
73                match self.repos.create(name, owner) {
74                    Ok(_repo) => {
75                        info!(owner = %owner, name = %name, "Created repository via consensus");
76
77                        // Emit event
78                        let repo_key = format!("{}/{}", owner, name);
79                        self.realtime.emit_event(
80                            format!("repo:{}", repo_key),
81                            EventKind::RepoCreated,
82                            serde_json::json!({
83                                "owner": owner,
84                                "name": name,
85                                "repository": repo_key
86                            }),
87                        );
88
89                        Ok(())
90                    }
91                    Err(e) => {
92                        warn!(owner = %owner, name = %name, error = %e, "Failed to create repository");
93                        Err(ConsensusError::TransactionFailed(format!(
94                            "create repository failed: {}",
95                            e
96                        )))
97                    }
98                }
99            }
100
101            Transaction::DeleteRepository {
102                repo_key,
103                deleter: _,
104                signature: _,
105            } => {
106                // Note: RepoStore doesn't have a delete method yet
107                // For now, just log the deletion
108                info!(repo_key = %repo_key, "Repository deletion requested via consensus");
109
110                // Emit event
111                self.realtime.emit_event(
112                    format!("repo:{}", repo_key),
113                    EventKind::Push, // Use Push for now as placeholder
114                    serde_json::json!({
115                        "type": "repository_deleted",
116                        "repository": repo_key
117                    }),
118                );
119
120                Ok(())
121            }
122
123            Transaction::CreateIssue {
124                repo_key,
125                title,
126                description,
127                author,
128                signer: _,
129                signature: _,
130            } => {
131                // For now, log the issue creation
132                // Full implementation would use collaboration.create_issue()
133                info!(
134                    repo_key = %repo_key,
135                    title = %title,
136                    author = %author,
137                    "Issue creation requested via consensus"
138                );
139
140                // Emit event
141                self.realtime.emit_event(
142                    format!("repo:{}", repo_key),
143                    EventKind::IssueOpened,
144                    serde_json::json!({
145                        "repository": repo_key,
146                        "title": title,
147                        "author": author,
148                        "description": description
149                    }),
150                );
151
152                Ok(())
153            }
154
155            Transaction::CreatePullRequest {
156                repo_key,
157                title,
158                description: _,
159                author,
160                source_branch,
161                target_branch,
162                source_commit: _,
163                target_commit: _,
164                signer: _,
165                signature: _,
166            } => {
167                // For now, log the PR creation
168                info!(
169                    repo_key = %repo_key,
170                    title = %title,
171                    author = %author,
172                    source_branch = %source_branch,
173                    target_branch = %target_branch,
174                    "PR creation requested via consensus"
175                );
176
177                // Emit event
178                self.realtime.emit_event(
179                    format!("repo:{}", repo_key),
180                    EventKind::PrOpened,
181                    serde_json::json!({
182                        "repository": repo_key,
183                        "title": title,
184                        "author": author,
185                        "source_branch": source_branch,
186                        "target_branch": target_branch
187                    }),
188                );
189
190                Ok(())
191            }
192
193            Transaction::CreateOrganization {
194                name,
195                display_name,
196                creator: _,
197                signature: _,
198            } => {
199                // For now, log the org creation
200                info!(
201                    name = %name,
202                    display_name = %display_name,
203                    "Organization creation requested via consensus"
204                );
205
206                Ok(())
207            }
208
209            // For now, log unimplemented transaction types
210            _ => {
211                debug!(
212                    kind = tx.kind(),
213                    "Transaction type not yet fully implemented"
214                );
215                Ok(())
216            }
217        }
218    }
219
220    /// Computes a simple state root from repository count.
221    fn compute_state_root_internal(&self) -> [u8; 32] {
222        use sha2::{Digest, Sha256};
223
224        let mut hasher = Sha256::new();
225
226        // Hash repository state
227        let repo_count = self.repos.list().len() as u64;
228        hasher.update(repo_count.to_le_bytes());
229
230        // Add current height
231        hasher.update(self.height.read().to_le_bytes());
232
233        let result = hasher.finalize();
234        let mut root = [0u8; 32];
235        root.copy_from_slice(&result);
236        root
237    }
238}
239
240#[async_trait]
241impl ConsensusApplication for GutsApplication {
242    /// Called when a block is finalized.
243    async fn on_block_finalized(&self, block: &FinalizedBlock) -> Result<()> {
244        let height = block.height();
245        let tx_count = block.block.tx_count();
246
247        info!(
248            height = height,
249            tx_count = tx_count,
250            block_id = %block.id(),
251            "Applying finalized block"
252        );
253
254        // Apply each transaction in order
255        for tx in &block.block.transactions {
256            if let Err(e) = self.apply_transaction(tx) {
257                error!(
258                    tx_id = %tx.id(),
259                    kind = tx.kind(),
260                    error = %e,
261                    "Failed to apply transaction"
262                );
263                // Continue with other transactions - failed transactions are logged but don't
264                // halt block application. In production, transaction verification should
265                // prevent invalid transactions from being included.
266            }
267        }
268
269        // Update height
270        *self.height.write() = height;
271
272        // Update state root
273        let new_root = self.compute_state_root_internal();
274        *self.state_root.write() = new_root;
275
276        debug!(
277            height = height,
278            state_root = hex::encode(new_root),
279            "Block application complete"
280        );
281
282        Ok(())
283    }
284
285    /// Computes the state root after applying transactions.
286    async fn compute_state_root(&self, _transactions: &[Transaction]) -> Result<[u8; 32]> {
287        // For now, return the current state root
288        // In production, we'd simulate applying transactions and return the resulting root
289        Ok(*self.state_root.read())
290    }
291
292    /// Verifies that a transaction is valid for inclusion.
293    async fn verify_transaction(&self, tx: &Transaction) -> Result<()> {
294        match tx {
295            Transaction::CreateRepository { owner, name, .. } => {
296                // Check if repo already exists
297                if self.repos.get(owner, name).is_ok() {
298                    return Err(ConsensusError::TransactionFailed(format!(
299                        "repository {}/{} already exists",
300                        owner, name
301                    )));
302                }
303            }
304            Transaction::DeleteRepository { repo_key, .. } => {
305                let parts: Vec<&str> = repo_key.split('/').collect();
306                if parts.len() != 2 {
307                    return Err(ConsensusError::TransactionFailed(
308                        "invalid repo_key format".into(),
309                    ));
310                }
311                // Check if repo exists
312                if self.repos.get(parts[0], parts[1]).is_err() {
313                    return Err(ConsensusError::TransactionFailed(format!(
314                        "repository {} does not exist",
315                        repo_key
316                    )));
317                }
318            }
319            _ => {
320                // Other transaction types: basic validation
321                // In production, we'd verify signatures, permissions, etc.
322            }
323        }
324
325        Ok(())
326    }
327
328    /// Gets the current height.
329    fn current_height(&self) -> u64 {
330        *self.height.read()
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    fn test_app() -> GutsApplication {
339        GutsApplication::new(
340            Arc::new(RepoStore::new()),
341            Arc::new(CollaborationStore::new()),
342            Arc::new(AuthStore::new()),
343            Arc::new(EventHub::new()),
344        )
345    }
346
347    #[test]
348    fn test_guts_application_creation() {
349        let app = test_app();
350        assert_eq!(app.current_height(), 0);
351    }
352
353    #[tokio::test]
354    async fn test_transaction_verification() {
355        let app = test_app();
356
357        // Create a test transaction
358        use commonware_cryptography::{ed25519, PrivateKeyExt, Signer};
359        use guts_consensus::{SerializablePublicKey, SerializableSignature};
360
361        let key = ed25519::PrivateKey::from_seed(42);
362        let sig = key.sign(Some(b"_GUTS"), b"test");
363
364        let tx = Transaction::CreateRepository {
365            owner: "alice".to_string(),
366            name: "test-repo".to_string(),
367            description: "A test repository".to_string(),
368            default_branch: "main".to_string(),
369            visibility: "public".to_string(),
370            creator: SerializablePublicKey::from_pubkey(&key.public_key()),
371            signature: SerializableSignature::from_signature(&sig),
372        };
373
374        // First creation should succeed
375        let result = app.verify_transaction(&tx).await;
376        assert!(result.is_ok());
377
378        // Apply the transaction
379        app.apply_transaction(&tx).unwrap();
380
381        // Second verification should fail (repo exists)
382        let result = app.verify_transaction(&tx).await;
383        assert!(result.is_err());
384    }
385
386    #[test]
387    fn test_state_root_computation() {
388        let app = test_app();
389
390        // Initial state root should be computed
391        let root1 = app.compute_state_root_internal();
392
393        // Create a repository
394        app.repos.create("test", "alice").unwrap();
395
396        // State root should change
397        let root2 = app.compute_state_root_internal();
398        assert_ne!(root1, root2);
399    }
400}