1use serde::{Deserialize, Serialize};
23
24use crate::Address;
25
26pub const MAX_COURSE_CODE_BYTES: usize = 32;
33pub const MAX_DEPARTMENT_BYTES: usize = 64;
35pub const MAX_TERM_BYTES: usize = 32;
37pub const MAX_SECTION_BYTES: usize = 32;
39pub const MAX_TITLE_BYTES: usize = 256;
41pub const MAX_EDU_OP_DATA_BYTES: usize = 64 * 1024;
43pub const MAX_MEMO_BYTES: usize = 1024;
45
46const DOMAIN_CATALOG_ID: &[u8] = b"SRC817-CATALOG:v1:";
49const DOMAIN_OFFERING_ID: &[u8] = b"SRC818-OFFERING:v1:";
50const DOMAIN_STUDENT_COMMITMENT: &[u8] = b"SRC818-STUDENT:v1:";
51const DOMAIN_SUBMISSION_COMMITMENT: &[u8] = b"SRC818-SUBMISSION:v1:";
52const DOMAIN_GRADE_COMMITMENT: &[u8] = b"SRC818-GRADE:v1:";
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
60#[repr(u8)]
61pub enum EducationStandard {
62 CourseCatalog = 0,
63 CourseOffering = 1,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct EducationTxData {
81 pub standard: EducationStandard,
82 pub operation: u16,
83 pub data: Vec<u8>,
84 pub recipient: Address,
85}
86
87pub mod catalog_op {
89 pub const CREATE_CATALOG_ENTRY: u16 = 0;
90 pub const UPDATE_CATALOG_ENTRY: u16 = 1;
91 pub const PUBLISH_CATALOG_CONTENT: u16 = 2;
92 pub const DEPRECATE_CATALOG_ENTRY: u16 = 3;
93 pub const SUPERSEDE_CATALOG_ENTRY: u16 = 4;
94 pub const ARCHIVE_CATALOG_ENTRY: u16 = 5;
95}
96
97pub mod offering_op {
99 pub const CREATE_OFFERING: u16 = 0;
100 pub const UPDATE_OFFERING: u16 = 1;
101 pub const PUBLISH_CONTENT: u16 = 2;
102 pub const ADD_ASSESSMENT: u16 = 3;
103 pub const UPDATE_ASSESSMENT: u16 = 4;
104 pub const OPEN_ENROLLMENT: u16 = 5;
105 pub const CLOSE_ENROLLMENT: u16 = 6;
106 pub const LINK_ENROLLMENT: u16 = 7;
107 pub const SUBMIT_ASSIGNMENT: u16 = 8;
108 pub const SUBMIT_EXAM: u16 = 9;
109 pub const GRADE_SUBMISSION: u16 = 10;
110 pub const FINALIZE_GRADE: u16 = 11;
111 pub const FINALIZE_COURSE: u16 = 12;
112 pub const ARCHIVE_OFFERING: u16 = 13;
113 pub const SUSPEND_OR_CANCEL_OFFERING: u16 = 14;
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122pub struct SnipRef {
123 pub content_root: [u8; 32],
124 pub snip_file_id: Option<[u8; 32]>,
125 pub size_bytes: u64,
126 pub schema_version: u32,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137pub enum AccessAudience {
138 Public,
139 EnrolledStudents,
140 InstructorsOnly,
141 StaffOnly,
142 IndividualStudent([u8; 32]),
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148pub struct ContentAccessPolicy {
149 pub opens_at: Option<u64>,
150 pub closes_at: Option<u64>,
151 pub grace_until: Option<u64>,
152 pub audience: AccessAudience,
153 pub revoke_on_course_archive: bool,
154}
155
156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160pub struct ManagedSnipRef {
161 pub snip_ref: SnipRef,
162 pub access_policy: ContentAccessPolicy,
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
172#[repr(u8)]
173pub enum CatalogStatus {
174 Draft = 0,
175 Active = 1,
176 Deprecated = 2,
177 Archived = 3,
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
181#[repr(u8)]
182pub enum OfferingStatus {
183 Draft = 0,
184 Active = 1,
185 EnrollmentClosed = 2,
186 Completed = 3,
187 Archived = 4,
188 Suspended = 5,
189 Cancelled = 6,
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
193#[repr(u8)]
194pub enum AssessmentKind {
195 Assignment = 0,
196 Exam = 1,
197 Quiz = 2,
198 Project = 3,
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
202#[repr(u8)]
203pub enum CourseRole {
204 InstitutionAdmin = 0,
205 Instructor = 1,
206 TeachingAssistant = 2,
207 Grader = 3,
208 Student = 4,
209 Auditor = 5,
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
213#[repr(u8)]
214pub enum CourseLevel {
215 Undergraduate = 0,
216 Graduate = 1,
217 Doctoral = 2,
218 Professional = 3,
219 Continuing = 4,
220 Other = 5,
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
224#[repr(u8)]
225pub enum ContentKind {
226 Syllabus = 0,
227 LectureMaterial = 1,
228 Reading = 2,
229 Resource = 3,
230 Other = 4,
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
237#[repr(u8)]
238pub enum SuspendCancelAction {
239 Suspend = 0,
240 Resume = 1,
241 Cancel = 2,
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub struct InvalidEducationCode(pub u8);
256
257impl core::fmt::Display for InvalidEducationCode {
258 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
259 write!(f, "invalid education wire code: {}", self.0)
260 }
261}
262
263impl TryFrom<u8> for CourseLevel {
264 type Error = InvalidEducationCode;
265 fn try_from(v: u8) -> Result<Self, Self::Error> {
266 Ok(match v {
267 0 => CourseLevel::Undergraduate,
268 1 => CourseLevel::Graduate,
269 2 => CourseLevel::Doctoral,
270 3 => CourseLevel::Professional,
271 4 => CourseLevel::Continuing,
272 5 => CourseLevel::Other,
273 other => return Err(InvalidEducationCode(other)),
274 })
275 }
276}
277
278impl TryFrom<u8> for ContentKind {
279 type Error = InvalidEducationCode;
280 fn try_from(v: u8) -> Result<Self, Self::Error> {
281 Ok(match v {
282 0 => ContentKind::Syllabus,
283 1 => ContentKind::LectureMaterial,
284 2 => ContentKind::Reading,
285 3 => ContentKind::Resource,
286 4 => ContentKind::Other,
287 other => return Err(InvalidEducationCode(other)),
288 })
289 }
290}
291
292impl TryFrom<u8> for AssessmentKind {
293 type Error = InvalidEducationCode;
294 fn try_from(v: u8) -> Result<Self, Self::Error> {
295 Ok(match v {
296 0 => AssessmentKind::Assignment,
297 1 => AssessmentKind::Exam,
298 2 => AssessmentKind::Quiz,
299 3 => AssessmentKind::Project,
300 other => return Err(InvalidEducationCode(other)),
301 })
302 }
303}
304
305impl TryFrom<u8> for CourseRole {
306 type Error = InvalidEducationCode;
307 fn try_from(v: u8) -> Result<Self, Self::Error> {
308 Ok(match v {
309 0 => CourseRole::InstitutionAdmin,
310 1 => CourseRole::Instructor,
311 2 => CourseRole::TeachingAssistant,
312 3 => CourseRole::Grader,
313 4 => CourseRole::Student,
314 5 => CourseRole::Auditor,
315 other => return Err(InvalidEducationCode(other)),
316 })
317 }
318}
319
320impl TryFrom<u8> for SuspendCancelAction {
321 type Error = InvalidEducationCode;
322 fn try_from(v: u8) -> Result<Self, Self::Error> {
323 Ok(match v {
324 0 => SuspendCancelAction::Suspend,
325 1 => SuspendCancelAction::Resume,
326 2 => SuspendCancelAction::Cancel,
327 other => return Err(InvalidEducationCode(other)),
328 })
329 }
330}
331
332#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
342pub struct CreateCatalogEntryData {
343 pub catalog_id: [u8; 32],
344 pub institution_id: [u8; 32],
345 pub department: String,
346 pub course_code: String,
347 pub course_title: Option<String>,
349 pub title_commitment: Option<[u8; 32]>,
350 pub course_level: u8,
352 pub credit_hours: Option<u16>,
355 pub credit_commitment: Option<[u8; 32]>,
356 pub prerequisites_count: u32,
359 pub prerequisites_root: Option<[u8; 32]>,
360 pub version: u32,
361 pub supersedes: Option<[u8; 32]>,
362 pub nonce: u64,
363}
364
365#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
367pub struct CreateOfferingData {
368 pub offering_id: [u8; 32],
369 pub catalog_id: [u8; 32],
371 pub term: String,
372 pub section: String,
373 pub instruction_start_at: u64,
376 pub instruction_end_at: u64,
377 pub final_grade_submission_deadline: u64,
378 pub nonce: u64,
379}
380
381#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
388pub struct SubmitAssignmentReceiptData {
389 pub offering_id: [u8; 32],
390 pub assessment_id: [u8; 32],
391 pub student_commitment: [u8; 32],
393 pub submission_commitment: [u8; 32],
394 pub work: ManagedSnipRef,
396 pub attempt: u16,
397 pub enrollment_ref: [u8; 32],
399 pub student_auth_commitment: Option<[u8; 32]>,
404}
405
406#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
418pub struct UpdateCatalogEntryData {
419 pub catalog_id: [u8; 32],
420 pub course_title: Option<String>,
421 pub title_commitment: Option<[u8; 32]>,
422 pub course_level: Option<u8>,
424 pub credit_hours: Option<u16>,
425 pub credit_commitment: Option<[u8; 32]>,
426 pub nonce: u64,
427}
428
429#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
432pub struct PublishCatalogContentData {
433 pub catalog_id: [u8; 32],
434 pub description_ref: Option<ManagedSnipRef>,
435 pub learning_outcomes_ref: Option<ManagedSnipRef>,
436 pub default_syllabus_ref: Option<ManagedSnipRef>,
437 pub default_assessment_policy_ref: Option<ManagedSnipRef>,
438 pub nonce: u64,
439}
440
441#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
443pub struct DeprecateCatalogEntryData {
444 pub catalog_id: [u8; 32],
445 pub nonce: u64,
446}
447
448#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
450pub struct SupersedeCatalogEntryData {
451 pub old_catalog_id: [u8; 32],
452 pub new_catalog_id: [u8; 32],
453 pub nonce: u64,
454}
455
456#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
458pub struct ArchiveCatalogEntryData {
459 pub catalog_id: [u8; 32],
460 pub nonce: u64,
461}
462
463#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
467pub struct UpdateOfferingData {
468 pub offering_id: [u8; 32],
469 pub term: Option<String>,
470 pub section: Option<String>,
471 pub instruction_start_at: Option<u64>,
472 pub instruction_end_at: Option<u64>,
473 pub final_grade_submission_deadline: Option<u64>,
474 pub nonce: u64,
475}
476
477#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
479pub struct PublishContentData {
480 pub offering_id: [u8; 32],
481 pub content_id: [u8; 32],
482 pub kind: u8,
484 pub item: ManagedSnipRef,
485 pub content_commitment: [u8; 32],
486 pub nonce: u64,
487}
488
489#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
491pub struct AddAssessmentData {
492 pub offering_id: [u8; 32],
493 pub assessment_id: [u8; 32],
494 pub kind: u8,
496 pub instructions: ManagedSnipRef,
497 pub spec_commitment: [u8; 32],
498 pub opens_at: u64,
499 pub due_at: u64,
500 pub max_attempts: u16,
501 pub weight_bps: u16,
502 pub answer_key_commitment: Option<[u8; 32]>,
503 pub answer_key_access: Option<ContentAccessPolicy>,
504 pub nonce: u64,
505}
506
507#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
509pub struct UpdateAssessmentData {
510 pub offering_id: [u8; 32],
511 pub assessment_id: [u8; 32],
512 pub opens_at: Option<u64>,
513 pub due_at: Option<u64>,
514 pub max_attempts: Option<u16>,
515 pub weight_bps: Option<u16>,
516 pub instructions: Option<ManagedSnipRef>,
517 pub nonce: u64,
518}
519
520#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
522pub struct OpenEnrollmentData {
523 pub offering_id: [u8; 32],
524 pub nonce: u64,
525}
526
527#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
529pub struct CloseEnrollmentData {
530 pub offering_id: [u8; 32],
531 pub nonce: u64,
532}
533
534#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
538pub struct LinkEnrollmentData {
539 pub offering_id: [u8; 32],
540 pub student_commitment: [u8; 32],
541 pub enrollment_ref: [u8; 32],
542 pub nonce: u64,
543}
544
545pub type SubmitExamReceiptData = SubmitAssignmentReceiptData;
550
551#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
555pub struct GradeSubmissionData {
556 pub offering_id: [u8; 32],
557 pub assessment_id: [u8; 32],
558 pub student_commitment: [u8; 32],
559 pub grade_commitment: [u8; 32],
560 pub feedback: Option<ManagedSnipRef>,
561 pub grader_role: u8,
563 pub nonce: u64,
564}
565
566#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
568pub struct FinalizeGradeData {
569 pub offering_id: [u8; 32],
570 pub assessment_id: [u8; 32],
571 pub student_commitment: [u8; 32],
572 pub nonce: u64,
573}
574
575#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
577pub struct FinalizeCourseData {
578 pub offering_id: [u8; 32],
579 pub nonce: u64,
580}
581
582#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
584pub struct ArchiveOfferingData {
585 pub offering_id: [u8; 32],
586 pub nonce: u64,
587}
588
589#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
593pub struct SuspendOrCancelOfferingData {
594 pub offering_id: [u8; 32],
595 pub action: u8,
597 pub nonce: u64,
598}
599
600fn blake3_domain_bincode<T: Serialize>(domain: &[u8], value: &T) -> [u8; 32] {
612 let inner = bincode::serialize(value)
614 .expect("commitment input is infallibly bincode-serializable");
615 let mut buf: Vec<u8> = Vec::with_capacity(domain.len() + inner.len());
616 buf.extend_from_slice(domain);
617 buf.extend_from_slice(&inner);
618 *blake3::hash(&buf).as_bytes()
619}
620
621pub fn catalog_id(
626 institution_id: &[u8; 32],
627 department: &str,
628 course_code: &str,
629 version: u32,
630 nonce: u64,
631) -> [u8; 32] {
632 blake3_domain_bincode(
633 DOMAIN_CATALOG_ID,
634 &(*institution_id, department, course_code, version, nonce),
635 )
636}
637
638pub fn offering_id(
642 catalog_id: &[u8; 32],
643 term: &str,
644 section: &str,
645 creator: &Address,
646 nonce: u64,
647) -> [u8; 32] {
648 blake3_domain_bincode(
649 DOMAIN_OFFERING_ID,
650 &(*catalog_id, term, section, *creator.as_bytes(), nonce),
651 )
652}
653
654pub fn student_commitment(
659 subject: &[u8; 32],
660 offering_id: &[u8; 32],
661 salt: &[u8; 32],
662) -> [u8; 32] {
663 blake3_domain_bincode(
664 DOMAIN_STUDENT_COMMITMENT,
665 &(*subject, *offering_id, *salt),
666 )
667}
668
669pub fn submission_commitment(
673 offering_id: &[u8; 32],
674 assessment_id: &[u8; 32],
675 student_commitment: &[u8; 32],
676 attempt: u16,
677 work_hash: &[u8; 32],
678 salt: &[u8; 32],
679) -> [u8; 32] {
680 blake3_domain_bincode(
681 DOMAIN_SUBMISSION_COMMITMENT,
682 &(
683 *offering_id,
684 *assessment_id,
685 *student_commitment,
686 attempt,
687 *work_hash,
688 *salt,
689 ),
690 )
691}
692
693pub fn grade_commitment(
698 offering_id: &[u8; 32],
699 assessment_id: &[u8; 32],
700 student_commitment: &[u8; 32],
701 grade_value: &[u8],
702 salt: &[u8; 32],
703) -> [u8; 32] {
704 blake3_domain_bincode(
705 DOMAIN_GRADE_COMMITMENT,
706 &(
707 *offering_id,
708 *assessment_id,
709 *student_commitment,
710 grade_value,
711 *salt,
712 ),
713 )
714}