Skip to content

Commit 6434168

Browse files
authored
Merge pull request #129 from Agbasimere/main
feat(#103): Backup and disaster recovery system
2 parents 8fb0d0f + 0b07361 commit 6434168

File tree

19 files changed

+844
-12
lines changed

19 files changed

+844
-12
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,18 @@ fn hello_returns_input() {
409409
- `curl not found` while funding
410410
- Install curl or fund the account manually using the friendbot URL
411411

412+
### Windows: linker or "export ordinal too large"
413+
414+
On Windows, `cargo test` may fail with **`link.exe` not found** (MSVC) or **`export ordinal too large: 79994`** (MinGW). The contract has many exports, which can exceed MinGW’s DLL limit.
415+
416+
- **Verify the contract (WASM only, no tests):**
417+
```powershell
418+
.\scripts\check-wasm.ps1
419+
```
420+
Or: `cargo build -p teachlink-contract --target wasm32-unknown-unknown`
421+
- **Run full tests:** Install [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) with "Desktop development with C++", then use the default (MSVC) toolchain and run `cargo test -p teachlink-contract`.
422+
- **Otherwise:** Rely on CI (GitHub Actions) for `cargo test`; the WASM build is what gets deployed.
423+
412424
## License
413425

414426
This project is licensed under the MIT License. See `LICENSE` for details.

contracts/teachlink/src/backup.rs

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
//! Backup and Disaster Recovery Module
2+
//!
3+
//! Provides backup scheduling, integrity verification, recovery recording,
4+
//! and audit trails for compliance. Off-chain systems use events to replicate
5+
//! data; this module records manifests, verification, and RTO recovery metrics.
6+
7+
use crate::audit::AuditManager;
8+
use crate::errors::BridgeError;
9+
use crate::events::{BackupCreatedEvent, BackupVerifiedEvent, RecoveryExecutedEvent};
10+
use crate::storage::{
11+
BACKUP_COUNTER, BACKUP_MANIFESTS, BACKUP_SCHEDULES, BACKUP_SCHED_CNT, RECOVERY_CNT,
12+
RECOVERY_RECORDS,
13+
};
14+
use crate::types::{BackupManifest, BackupSchedule, OperationType, RecoveryRecord, RtoTier};
15+
use soroban_sdk::{Address, Bytes, Env, Map, Vec};
16+
17+
/// Backup and disaster recovery manager
18+
pub struct BackupManager;
19+
20+
impl BackupManager {
21+
/// Create a backup manifest (authorized caller). Integrity hash is supplied by off-chain.
22+
pub fn create_backup(
23+
env: &Env,
24+
creator: Address,
25+
integrity_hash: Bytes,
26+
rto_tier: RtoTier,
27+
encryption_ref: u64,
28+
) -> Result<u64, BridgeError> {
29+
creator.require_auth();
30+
31+
let mut counter: u64 = env
32+
.storage()
33+
.instance()
34+
.get(&BACKUP_COUNTER)
35+
.unwrap_or(0u64);
36+
counter += 1;
37+
38+
let manifest = BackupManifest {
39+
backup_id: counter,
40+
created_at: env.ledger().timestamp(),
41+
created_by: creator.clone(),
42+
integrity_hash: integrity_hash.clone(),
43+
rto_tier: rto_tier.clone(),
44+
encryption_ref,
45+
};
46+
47+
let mut manifests: Map<u64, BackupManifest> = env
48+
.storage()
49+
.instance()
50+
.get(&BACKUP_MANIFESTS)
51+
.unwrap_or_else(|| Map::new(env));
52+
manifests.set(counter, manifest);
53+
env.storage().instance().set(&BACKUP_MANIFESTS, &manifests);
54+
env.storage().instance().set(&BACKUP_COUNTER, &counter);
55+
56+
BackupCreatedEvent {
57+
backup_id: counter,
58+
created_by: creator.clone(),
59+
integrity_hash,
60+
rto_tier: rto_tier.clone(),
61+
created_at: env.ledger().timestamp(),
62+
}
63+
.publish(env);
64+
65+
let details = Bytes::from_slice(env, &counter.to_be_bytes());
66+
AuditManager::create_audit_record(
67+
env,
68+
OperationType::BackupCreated,
69+
creator,
70+
details,
71+
Bytes::new(env),
72+
)?;
73+
74+
Ok(counter)
75+
}
76+
77+
/// Get backup manifest by id
78+
pub fn get_backup_manifest(env: &Env, backup_id: u64) -> Option<BackupManifest> {
79+
let manifests: Map<u64, BackupManifest> = env
80+
.storage()
81+
.instance()
82+
.get(&BACKUP_MANIFESTS)
83+
.unwrap_or_else(|| Map::new(env));
84+
manifests.get(backup_id)
85+
}
86+
87+
/// Verify backup integrity (compare expected hash to stored). Emit event and audit.
88+
pub fn verify_backup(
89+
env: &Env,
90+
backup_id: u64,
91+
verifier: Address,
92+
expected_hash: Bytes,
93+
) -> Result<bool, BridgeError> {
94+
verifier.require_auth();
95+
96+
let manifest =
97+
Self::get_backup_manifest(env, backup_id).ok_or(BridgeError::InvalidInput)?;
98+
let valid = manifest.integrity_hash == expected_hash;
99+
100+
BackupVerifiedEvent {
101+
backup_id,
102+
verified_by: verifier.clone(),
103+
verified_at: env.ledger().timestamp(),
104+
valid,
105+
}
106+
.publish(env);
107+
108+
let details = Bytes::from_slice(env, &[if valid { 1u8 } else { 0u8 }]);
109+
AuditManager::create_audit_record(
110+
env,
111+
OperationType::BackupVerified,
112+
verifier,
113+
details,
114+
Bytes::new(env),
115+
)?;
116+
117+
Ok(valid)
118+
}
119+
120+
/// Schedule automated backup (owner auth)
121+
pub fn schedule_backup(
122+
env: &Env,
123+
owner: Address,
124+
next_run_at: u64,
125+
interval_seconds: u64,
126+
rto_tier: RtoTier,
127+
) -> Result<u64, BridgeError> {
128+
owner.require_auth();
129+
130+
let mut counter: u64 = env
131+
.storage()
132+
.instance()
133+
.get(&BACKUP_SCHED_CNT)
134+
.unwrap_or(0u64);
135+
counter += 1;
136+
137+
let schedule = BackupSchedule {
138+
schedule_id: counter,
139+
owner: owner.clone(),
140+
next_run_at,
141+
interval_seconds,
142+
rto_tier: rto_tier.clone(),
143+
enabled: true,
144+
created_at: env.ledger().timestamp(),
145+
};
146+
147+
let mut schedules: Map<u64, BackupSchedule> = env
148+
.storage()
149+
.instance()
150+
.get(&BACKUP_SCHEDULES)
151+
.unwrap_or_else(|| Map::new(env));
152+
schedules.set(counter, schedule);
153+
env.storage().instance().set(&BACKUP_SCHEDULES, &schedules);
154+
env.storage().instance().set(&BACKUP_SCHED_CNT, &counter);
155+
156+
Ok(counter)
157+
}
158+
159+
/// Get scheduled backups for an owner
160+
pub fn get_scheduled_backups(env: &Env, owner: Address) -> Vec<BackupSchedule> {
161+
let schedules: Map<u64, BackupSchedule> = env
162+
.storage()
163+
.instance()
164+
.get(&BACKUP_SCHEDULES)
165+
.unwrap_or_else(|| Map::new(env));
166+
167+
let mut result = Vec::new(env);
168+
for (_id, s) in schedules.iter() {
169+
if s.owner == owner {
170+
result.push_back(s);
171+
}
172+
}
173+
result
174+
}
175+
176+
/// Record a recovery execution (RTO tracking and audit trail)
177+
pub fn record_recovery(
178+
env: &Env,
179+
backup_id: u64,
180+
executed_by: Address,
181+
recovery_duration_secs: u64,
182+
success: bool,
183+
) -> Result<u64, BridgeError> {
184+
executed_by.require_auth();
185+
186+
if Self::get_backup_manifest(env, backup_id).is_none() {
187+
return Err(BridgeError::InvalidInput);
188+
}
189+
190+
let mut counter: u64 = env.storage().instance().get(&RECOVERY_CNT).unwrap_or(0u64);
191+
counter += 1;
192+
193+
let record = RecoveryRecord {
194+
recovery_id: counter,
195+
backup_id,
196+
executed_at: env.ledger().timestamp(),
197+
executed_by: executed_by.clone(),
198+
recovery_duration_secs,
199+
success,
200+
};
201+
202+
let mut records: Map<u64, RecoveryRecord> = env
203+
.storage()
204+
.instance()
205+
.get(&RECOVERY_RECORDS)
206+
.unwrap_or_else(|| Map::new(env));
207+
records.set(counter, record);
208+
env.storage().instance().set(&RECOVERY_RECORDS, &records);
209+
env.storage().instance().set(&RECOVERY_CNT, &counter);
210+
211+
RecoveryExecutedEvent {
212+
recovery_id: counter,
213+
backup_id,
214+
executed_by: executed_by.clone(),
215+
recovery_duration_secs,
216+
success,
217+
}
218+
.publish(env);
219+
220+
let details = Bytes::from_slice(env, &recovery_duration_secs.to_be_bytes());
221+
AuditManager::create_audit_record(
222+
env,
223+
OperationType::RecoveryExecuted,
224+
executed_by,
225+
details,
226+
Bytes::new(env),
227+
)?;
228+
229+
Ok(counter)
230+
}
231+
232+
/// Get recovery records (for audit trail and RTO reporting)
233+
pub fn get_recovery_records(env: &Env, limit: u32) -> Vec<RecoveryRecord> {
234+
let counter: u64 = env.storage().instance().get(&RECOVERY_CNT).unwrap_or(0u64);
235+
let records: Map<u64, RecoveryRecord> = env
236+
.storage()
237+
.instance()
238+
.get(&RECOVERY_RECORDS)
239+
.unwrap_or_else(|| Map::new(env));
240+
241+
let mut result = Vec::new(env);
242+
let start = if counter > limit as u64 {
243+
counter - limit as u64
244+
} else {
245+
1
246+
};
247+
for id in start..=counter {
248+
if let Some(r) = records.get(id) {
249+
result.push_back(r);
250+
}
251+
}
252+
result
253+
}
254+
255+
/// Get recent backup manifests (for monitoring and compliance)
256+
pub fn get_recent_backups(env: &Env, limit: u32) -> Vec<BackupManifest> {
257+
let counter: u64 = env
258+
.storage()
259+
.instance()
260+
.get(&BACKUP_COUNTER)
261+
.unwrap_or(0u64);
262+
let manifests: Map<u64, BackupManifest> = env
263+
.storage()
264+
.instance()
265+
.get(&BACKUP_MANIFESTS)
266+
.unwrap_or_else(|| Map::new(env));
267+
268+
let mut result = Vec::new(env);
269+
let start = if counter > limit as u64 {
270+
counter - limit as u64
271+
} else {
272+
1
273+
};
274+
for id in start..=counter {
275+
if let Some(m) = manifests.get(id) {
276+
result.push_back(m);
277+
}
278+
}
279+
result
280+
}
281+
}

contracts/teachlink/src/events.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,3 +454,34 @@ pub struct AlertTriggeredEvent {
454454
pub threshold: i128,
455455
pub triggered_at: u64,
456456
}
457+
458+
// ================= Backup and Disaster Recovery Events =================
459+
460+
#[contractevent]
461+
#[derive(Clone, Debug)]
462+
pub struct BackupCreatedEvent {
463+
pub backup_id: u64,
464+
pub created_by: Address,
465+
pub integrity_hash: Bytes,
466+
pub rto_tier: crate::types::RtoTier,
467+
pub created_at: u64,
468+
}
469+
470+
#[contractevent]
471+
#[derive(Clone, Debug)]
472+
pub struct BackupVerifiedEvent {
473+
pub backup_id: u64,
474+
pub verified_by: Address,
475+
pub verified_at: u64,
476+
pub valid: bool,
477+
}
478+
479+
#[contractevent]
480+
#[derive(Clone, Debug)]
481+
pub struct RecoveryExecutedEvent {
482+
pub recovery_id: u64,
483+
pub backup_id: u64,
484+
pub executed_by: Address,
485+
pub recovery_duration_secs: u64,
486+
pub success: bool,
487+
}

0 commit comments

Comments
 (0)