use serde::{Deserialize, Serialize};
use crate::{Address, Timestamp};
pub type EmploymentId = [u8; 32];
pub type IncomeAttestationId = [u8; 32];
pub type PolicyId = [u8; 32];
pub type ProofId = [u8; 32];
pub type SubjectRef = [u8; 32];
pub type EmployerRef = [u8; 32];
pub const EMPLOYMENT_CREDENTIAL_DOMAIN_SEP: &[u8] = b"SRC882-EMPLOYMENT-v1";
pub const INCOME_ATTESTATION_DOMAIN_SEP: &[u8] = b"SRC883-INCOME-v1";
pub const TENURE_COMMITMENT_DOMAIN_SEP: &[u8] = b"SRC882-TENURE-v1";
pub const ROLE_COMMITMENT_DOMAIN_SEP: &[u8] = b"SRC882-ROLE-v1";
pub const INCOME_BRACKET_DOMAIN_SEP: &[u8] = b"SRC883-BRACKET-v1";
pub const PERIOD_COMMITMENT_DOMAIN_SEP: &[u8] = b"SRC883-PERIOD-v1";
pub const EMPLOYMENT_PROOF_DOMAIN_SEP: &[u8] = b"SRC885-PROOF-v1";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum EmploymentIssuerClass {
GovernmentLabor = 0,
PayrollProcessor = 1,
RegulatedHrPlatform = 2,
Peo = 3,
Employer = 10,
HrPlatform = 11,
StaffingAgency = 12,
GigPlatform = 13,
}
impl EmploymentIssuerClass {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(EmploymentIssuerClass::GovernmentLabor),
1 => Some(EmploymentIssuerClass::PayrollProcessor),
2 => Some(EmploymentIssuerClass::RegulatedHrPlatform),
3 => Some(EmploymentIssuerClass::Peo),
10 => Some(EmploymentIssuerClass::Employer),
11 => Some(EmploymentIssuerClass::HrPlatform),
12 => Some(EmploymentIssuerClass::StaffingAgency),
13 => Some(EmploymentIssuerClass::GigPlatform),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
EmploymentIssuerClass::GovernmentLabor => "Government Labor Department",
EmploymentIssuerClass::PayrollProcessor => "Payroll Processor",
EmploymentIssuerClass::RegulatedHrPlatform => "Regulated HR Platform",
EmploymentIssuerClass::Peo => "Professional Employer Organization",
EmploymentIssuerClass::Employer => "Employer",
EmploymentIssuerClass::HrPlatform => "HR Platform",
EmploymentIssuerClass::StaffingAgency => "Staffing Agency",
EmploymentIssuerClass::GigPlatform => "Gig Platform",
}
}
pub fn is_official(&self) -> bool {
matches!(
self,
EmploymentIssuerClass::GovernmentLabor
| EmploymentIssuerClass::PayrollProcessor
| EmploymentIssuerClass::RegulatedHrPlatform
| EmploymentIssuerClass::Peo
)
}
pub fn is_lowkey(&self) -> bool {
!self.is_official()
}
pub fn default_risk_level(&self) -> EmploymentRiskLevel {
match self {
EmploymentIssuerClass::GovernmentLabor => EmploymentRiskLevel::Low,
EmploymentIssuerClass::PayrollProcessor => EmploymentRiskLevel::Low,
EmploymentIssuerClass::RegulatedHrPlatform => EmploymentRiskLevel::Medium,
EmploymentIssuerClass::Peo => EmploymentRiskLevel::Medium,
EmploymentIssuerClass::Employer => EmploymentRiskLevel::Medium,
EmploymentIssuerClass::HrPlatform => EmploymentRiskLevel::High,
EmploymentIssuerClass::StaffingAgency => EmploymentRiskLevel::Medium,
EmploymentIssuerClass::GigPlatform => EmploymentRiskLevel::High,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum EmploymentRiskLevel {
Low = 0,
Medium = 1,
High = 2,
Critical = 3,
}
impl EmploymentRiskLevel {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(EmploymentRiskLevel::Low),
1 => Some(EmploymentRiskLevel::Medium),
2 => Some(EmploymentRiskLevel::High),
3 => Some(EmploymentRiskLevel::Critical),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
EmploymentRiskLevel::Low => "Low",
EmploymentRiskLevel::Medium => "Medium",
EmploymentRiskLevel::High => "High",
EmploymentRiskLevel::Critical => "Critical",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EmploymentIssuerProfile {
pub issuer_address: Address,
pub issuer_class: EmploymentIssuerClass,
#[serde(default)]
pub display_name: String,
pub issuer_commitment: [u8; 32],
pub jurisdiction_code: String,
pub policy_id: PolicyId,
pub status: IssuerStatus,
pub registered_at_height: u64,
pub created_at: Timestamp,
pub updated_at: Timestamp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum IssuerStatus {
Pending = 0,
Active = 1,
Suspended = 2,
Revoked = 3,
}
impl IssuerStatus {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(IssuerStatus::Pending),
1 => Some(IssuerStatus::Active),
2 => Some(IssuerStatus::Suspended),
3 => Some(IssuerStatus::Revoked),
_ => None,
}
}
pub fn is_active(&self) -> bool {
matches!(self, IssuerStatus::Active)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum EmploymentStatus {
Active = 0,
Suspended = 1,
Ended = 2,
OnLeave = 3,
}
impl EmploymentStatus {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(EmploymentStatus::Active),
1 => Some(EmploymentStatus::Suspended),
2 => Some(EmploymentStatus::Ended),
3 => Some(EmploymentStatus::OnLeave),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
EmploymentStatus::Active => "Active",
EmploymentStatus::Suspended => "Suspended",
EmploymentStatus::Ended => "Ended",
EmploymentStatus::OnLeave => "On Leave",
}
}
pub fn is_currently_employed(&self) -> bool {
matches!(self, EmploymentStatus::Active | EmploymentStatus::OnLeave)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EmploymentCredential {
pub employment_id: EmploymentId,
pub employee_address: Address,
pub employee_ref: SubjectRef,
pub employer_ref: EmployerRef,
pub status: EmploymentStatus,
pub tenure_commitment: [u8; 32],
pub role_commitment: Option<[u8; 32]>,
pub employment_type: EmploymentType,
pub valid_from: Timestamp,
pub expiry: Timestamp,
pub policy_id: PolicyId,
pub revocation_ref: Option<[u8; 32]>,
pub issuer_address: Address,
#[serde(default)]
pub issuer_name: String,
pub issuer_class: EmploymentIssuerClass,
pub created_at: Timestamp,
pub updated_at: Timestamp,
}
impl EmploymentCredential {
pub fn generate_id(
employee_ref: &SubjectRef,
employer_ref: &EmployerRef,
tenure_commitment: &[u8; 32],
nonce: u64,
) -> EmploymentId {
let mut hasher = blake3::Hasher::new();
hasher.update(EMPLOYMENT_CREDENTIAL_DOMAIN_SEP);
hasher.update(employee_ref);
hasher.update(employer_ref);
hasher.update(tenure_commitment);
hasher.update(&nonce.to_le_bytes());
*hasher.finalize().as_bytes()
}
pub fn generate_tenure_commitment(start_date: Timestamp, salt: &[u8; 32]) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(TENURE_COMMITMENT_DOMAIN_SEP);
hasher.update(&start_date.to_le_bytes());
hasher.update(salt);
*hasher.finalize().as_bytes()
}
pub fn generate_role_commitment(role_title: &str, department: &str, salt: &[u8; 32]) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(ROLE_COMMITMENT_DOMAIN_SEP);
hasher.update(role_title.as_bytes());
hasher.update(b":");
hasher.update(department.as_bytes());
hasher.update(salt);
*hasher.finalize().as_bytes()
}
pub fn is_valid(&self, current_time: Timestamp) -> bool {
current_time >= self.valid_from
&& (self.expiry == 0 || current_time < self.expiry)
&& self.revocation_ref.is_none()
&& self.status.is_currently_employed()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum EmploymentType {
FullTime = 0,
PartTime = 1,
Contract = 2,
Temporary = 3,
Internship = 4,
Freelance = 5,
Gig = 6,
}
impl EmploymentType {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(EmploymentType::FullTime),
1 => Some(EmploymentType::PartTime),
2 => Some(EmploymentType::Contract),
3 => Some(EmploymentType::Temporary),
4 => Some(EmploymentType::Internship),
5 => Some(EmploymentType::Freelance),
6 => Some(EmploymentType::Gig),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
EmploymentType::FullTime => "Full-time",
EmploymentType::PartTime => "Part-time",
EmploymentType::Contract => "Contract",
EmploymentType::Temporary => "Temporary",
EmploymentType::Internship => "Internship",
EmploymentType::Freelance => "Freelance",
EmploymentType::Gig => "Gig",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum IncomeBracket {
Bracket0 = 0,
Bracket1 = 1,
Bracket2 = 2,
Bracket3 = 3,
Bracket4 = 4,
Bracket5 = 5,
Bracket6 = 6,
Bracket7 = 7,
Bracket8 = 8,
Custom = 255,
}
impl IncomeBracket {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(IncomeBracket::Bracket0),
1 => Some(IncomeBracket::Bracket1),
2 => Some(IncomeBracket::Bracket2),
3 => Some(IncomeBracket::Bracket3),
4 => Some(IncomeBracket::Bracket4),
5 => Some(IncomeBracket::Bracket5),
6 => Some(IncomeBracket::Bracket6),
7 => Some(IncomeBracket::Bracket7),
8 => Some(IncomeBracket::Bracket8),
255 => Some(IncomeBracket::Custom),
_ => None,
}
}
pub fn description(&self) -> &'static str {
match self {
IncomeBracket::Bracket0 => "Below $25,000",
IncomeBracket::Bracket1 => "$25,000 - $50,000",
IncomeBracket::Bracket2 => "$50,000 - $75,000",
IncomeBracket::Bracket3 => "$75,000 - $100,000",
IncomeBracket::Bracket4 => "$100,000 - $150,000",
IncomeBracket::Bracket5 => "$150,000 - $200,000",
IncomeBracket::Bracket6 => "$200,000 - $300,000",
IncomeBracket::Bracket7 => "$300,000 - $500,000",
IncomeBracket::Bracket8 => "Above $500,000",
IncomeBracket::Custom => "Custom threshold",
}
}
pub fn is_at_least(&self, other: &IncomeBracket) -> bool {
(*self as u8) >= (*other as u8) && *self != IncomeBracket::Custom
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum IncomePeriod {
Monthly = 0,
Quarterly = 1,
Annual = 2,
YearToDate = 3,
}
impl IncomePeriod {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(IncomePeriod::Monthly),
1 => Some(IncomePeriod::Quarterly),
2 => Some(IncomePeriod::Annual),
3 => Some(IncomePeriod::YearToDate),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
IncomePeriod::Monthly => "Monthly",
IncomePeriod::Quarterly => "Quarterly",
IncomePeriod::Annual => "Annual",
IncomePeriod::YearToDate => "Year-to-Date",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IncomeAttestation {
pub attestation_id: IncomeAttestationId,
pub holder_address: Address,
pub subject_ref: SubjectRef,
pub period_commitment: [u8; 32],
pub period_type: IncomePeriod,
pub income_bracket: IncomeBracket,
pub threshold_commitment: Option<[u8; 32]>,
pub employment_id: Option<EmploymentId>,
pub issuer_address: Address,
pub issuer_class: EmploymentIssuerClass,
pub valid_from: Timestamp,
pub expiry: Timestamp,
pub policy_id: PolicyId,
pub revocation_ref: Option<[u8; 32]>,
pub created_at: Timestamp,
pub updated_at: Timestamp,
}
impl IncomeAttestation {
pub fn generate_id(
subject_ref: &SubjectRef,
period_commitment: &[u8; 32],
income_bracket: IncomeBracket,
nonce: u64,
) -> IncomeAttestationId {
let mut hasher = blake3::Hasher::new();
hasher.update(INCOME_ATTESTATION_DOMAIN_SEP);
hasher.update(subject_ref);
hasher.update(period_commitment);
hasher.update(&[income_bracket as u8]);
hasher.update(&nonce.to_le_bytes());
*hasher.finalize().as_bytes()
}
pub fn generate_period_commitment(
period_type: IncomePeriod,
start_date: Timestamp,
end_date: Timestamp,
salt: &[u8; 32],
) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(PERIOD_COMMITMENT_DOMAIN_SEP);
hasher.update(&[period_type as u8]);
hasher.update(&start_date.to_le_bytes());
hasher.update(&end_date.to_le_bytes());
hasher.update(salt);
*hasher.finalize().as_bytes()
}
pub fn generate_threshold_commitment(
threshold_min: u64,
threshold_max: u64,
currency: &str,
salt: &[u8; 32],
) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(INCOME_BRACKET_DOMAIN_SEP);
hasher.update(&threshold_min.to_le_bytes());
hasher.update(&threshold_max.to_le_bytes());
hasher.update(currency.as_bytes());
hasher.update(salt);
*hasher.finalize().as_bytes()
}
pub fn is_valid(&self, current_time: Timestamp) -> bool {
current_time >= self.valid_from
&& current_time < self.expiry
&& self.revocation_ref.is_none()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum EmploymentProofType {
CurrentlyEmployed = 0,
EmployedForDuration = 1,
IncomeInBracket = 2,
RoleInSet = 3,
EmploymentType = 4,
Combined = 255,
}
impl EmploymentProofType {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(EmploymentProofType::CurrentlyEmployed),
1 => Some(EmploymentProofType::EmployedForDuration),
2 => Some(EmploymentProofType::IncomeInBracket),
3 => Some(EmploymentProofType::RoleInSet),
4 => Some(EmploymentProofType::EmploymentType),
255 => Some(EmploymentProofType::Combined),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EmploymentProofProfile {
pub profile_id: [u8; 32],
pub proof_type: EmploymentProofType,
pub employer_scope: Option<[u8; 32]>,
pub min_duration_secs: Option<u64>,
pub min_income_bracket: Option<IncomeBracket>,
pub role_set_commitment: Option<[u8; 32]>,
pub required_employment_types: Vec<EmploymentType>,
pub required_issuer_classes: Vec<EmploymentIssuerClass>,
pub max_credential_age_secs: u64,
pub policy_id: PolicyId,
}
impl EmploymentProofProfile {
pub fn generate_id(&self) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(EMPLOYMENT_PROOF_DOMAIN_SEP);
hasher.update(&[self.proof_type.clone() as u8]);
if let Some(scope) = &self.employer_scope {
hasher.update(scope);
}
if let Some(duration) = self.min_duration_secs {
hasher.update(&duration.to_le_bytes());
}
if let Some(bracket) = &self.min_income_bracket {
hasher.update(&[*bracket as u8]);
}
hasher.update(&self.policy_id);
*hasher.finalize().as_bytes()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EmploymentProofEnvelope {
pub proof_id: ProofId,
pub profile_id: [u8; 32],
pub proof_type: EmploymentProofType,
pub subject_nullifier: [u8; 32],
pub proof_data: Vec<u8>,
pub public_inputs_commitment: [u8; 32],
pub credential_refs: Vec<[u8; 32]>,
pub source_issuer_class: EmploymentIssuerClass,
pub policy_id: PolicyId,
pub valid_from: Timestamp,
pub expiry: Timestamp,
pub created_at: Timestamp,
}
impl EmploymentProofEnvelope {
pub fn generate_id(
profile_id: &[u8; 32],
subject_nullifier: &[u8; 32],
proof_data: &[u8],
nonce: u64,
) -> ProofId {
let mut hasher = blake3::Hasher::new();
hasher.update(EMPLOYMENT_PROOF_DOMAIN_SEP);
hasher.update(profile_id);
hasher.update(subject_nullifier);
hasher.update(&blake3::hash(proof_data).as_bytes()[..]);
hasher.update(&nonce.to_le_bytes());
*hasher.finalize().as_bytes()
}
pub fn is_valid(&self, current_time: Timestamp) -> bool {
current_time >= self.valid_from && current_time < self.expiry
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum EmploymentEvent {
IssuerRegistered {
issuer_address: Address,
issuer_class: EmploymentIssuerClass,
timestamp: Timestamp,
},
IssuerStatusUpdated {
issuer_address: Address,
old_status: IssuerStatus,
new_status: IssuerStatus,
timestamp: Timestamp,
},
EmploymentCreated {
employment_id: EmploymentId,
employee_ref: SubjectRef,
employer_ref: EmployerRef,
status: EmploymentStatus,
timestamp: Timestamp,
},
EmploymentUpdated {
employment_id: EmploymentId,
old_status: EmploymentStatus,
new_status: EmploymentStatus,
timestamp: Timestamp,
},
EmploymentRevoked {
employment_id: EmploymentId,
revocation_ref: [u8; 32],
timestamp: Timestamp,
},
IncomeAttestationCreated {
attestation_id: IncomeAttestationId,
subject_ref: SubjectRef,
income_bracket: IncomeBracket,
timestamp: Timestamp,
},
IncomeAttestationRevoked {
attestation_id: IncomeAttestationId,
revocation_ref: [u8; 32],
timestamp: Timestamp,
},
ProofSubmitted {
proof_id: ProofId,
proof_type: EmploymentProofType,
timestamp: Timestamp,
},
ProofVerified {
proof_id: ProofId,
verifier: Address,
result: bool,
timestamp: Timestamp,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum EmploymentOperation {
RegisterIssuer = 0,
UpdateIssuer = 1,
SuspendIssuer = 2,
RevokeIssuer = 3,
ReactivateIssuer = 4,
CreateEmployment = 10,
UpdateEmployment = 11,
SuspendEmployment = 12,
EndEmployment = 13,
RevokeEmployment = 14,
CreateIncomeAttestation = 20,
UpdateIncomeAttestation = 21,
RevokeIncomeAttestation = 22,
SubmitProof = 30,
VerifyProof = 31,
}
impl EmploymentOperation {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(EmploymentOperation::RegisterIssuer),
1 => Some(EmploymentOperation::UpdateIssuer),
2 => Some(EmploymentOperation::SuspendIssuer),
3 => Some(EmploymentOperation::RevokeIssuer),
4 => Some(EmploymentOperation::ReactivateIssuer),
10 => Some(EmploymentOperation::CreateEmployment),
11 => Some(EmploymentOperation::UpdateEmployment),
12 => Some(EmploymentOperation::SuspendEmployment),
13 => Some(EmploymentOperation::EndEmployment),
14 => Some(EmploymentOperation::RevokeEmployment),
20 => Some(EmploymentOperation::CreateIncomeAttestation),
21 => Some(EmploymentOperation::UpdateIncomeAttestation),
22 => Some(EmploymentOperation::RevokeIncomeAttestation),
30 => Some(EmploymentOperation::SubmitProof),
31 => Some(EmploymentOperation::VerifyProof),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EmploymentTxData {
pub operation: EmploymentOperation,
pub data: Vec<u8>,
pub recipient: crate::Address,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_issuer_class_risk_levels() {
assert_eq!(
EmploymentIssuerClass::GovernmentLabor.default_risk_level(),
EmploymentRiskLevel::Low
);
assert_eq!(
EmploymentIssuerClass::GigPlatform.default_risk_level(),
EmploymentRiskLevel::High
);
}
#[test]
fn test_issuer_class_phases() {
assert!(EmploymentIssuerClass::PayrollProcessor.is_official());
assert!(!EmploymentIssuerClass::PayrollProcessor.is_lowkey());
assert!(EmploymentIssuerClass::Employer.is_lowkey());
assert!(!EmploymentIssuerClass::Employer.is_official());
}
#[test]
fn test_employment_id_generation() {
let employee_ref = [1u8; 32];
let employer_ref = [2u8; 32];
let tenure_commitment = [3u8; 32];
let id = EmploymentCredential::generate_id(&employee_ref, &employer_ref, &tenure_commitment, 1);
assert_ne!(id, [0u8; 32]);
let id2 = EmploymentCredential::generate_id(&employee_ref, &employer_ref, &tenure_commitment, 2);
assert_ne!(id, id2);
}
#[test]
fn test_tenure_commitment_generation() {
let start_date: Timestamp = 1700000000;
let salt = [4u8; 32];
let commitment = EmploymentCredential::generate_tenure_commitment(start_date, &salt);
assert_ne!(commitment, [0u8; 32]);
}
#[test]
fn test_income_bracket_ordering() {
assert!(IncomeBracket::Bracket5.is_at_least(&IncomeBracket::Bracket3));
assert!(!IncomeBracket::Bracket2.is_at_least(&IncomeBracket::Bracket5));
assert!(!IncomeBracket::Custom.is_at_least(&IncomeBracket::Bracket1));
}
#[test]
fn test_income_attestation_id_generation() {
let subject_ref = [5u8; 32];
let period_commitment = [6u8; 32];
let id = IncomeAttestation::generate_id(
&subject_ref,
&period_commitment,
IncomeBracket::Bracket3,
1,
);
assert_ne!(id, [0u8; 32]);
}
#[test]
fn test_employment_status_checks() {
assert!(EmploymentStatus::Active.is_currently_employed());
assert!(EmploymentStatus::OnLeave.is_currently_employed());
assert!(!EmploymentStatus::Ended.is_currently_employed());
assert!(!EmploymentStatus::Suspended.is_currently_employed());
}
}