Canonical contract interface reference for SDK clients. Target platform: Soroban (Stellar) smart contracts — Rust → WASM. Circuit backend: Groth16 over BN254.
- Identifiers & Addresses
- Entrypoints by Contract
- Data Structures
- Serialization Rules
- Events
- Error Reference
- Compatibility & Versioning
| Property | Value |
|---|---|
| Encoding | Stellar StrKey (e.g. G... for Ed25519, C... for contract) |
| SDK format | 32-byte public key (Ed25519) or 32-byte contract hash + 1-byte type |
| Conversion | stellar-strkey crate for encode/decode; stellar-xdr for binary |
Addresses are the native Soroban Address type. SDKs should use the Stellar SDK's
Address type when available, or the equivalent stellar-strkey library.
- Type:
u64(unsigned 64-bit integer) - Allocation: Sequential, auto-incremented (starting at
0) - Encoding in XDR:
Uint64(8 bytes big-endian) - Uniqueness: Global — company IDs are not scoped per contract
Employees are identified by their Soroban Address (Ed25519 public key).
- Type:
u32(unsigned 32-bit integer) - Allocation: Sequential per company (starting at
1) - Scoping: Period IDs are scoped within a company —
(company_id, period_id)forms the composite key
Company identifiers in the AuditModule use Soroban Symbol (max 32 bytes UTF-8).
Source: contracts/payroll_registry/src/lib.rs
Contract ID: deployed as payroll_registry.wasm
| Field | Type | Description |
|---|---|---|
admin |
Address |
Company admin (must sign) |
treasury |
Address |
Payment source address for token transfers |
| Returns | u64 |
Newly assigned company ID |
Behavior:
- Requires
admin.require_auth()— the caller must be the admin address. - Company IDs are allocated sequentially starting from
0. - Returns the assigned company ID.
Errors: None (always succeeds).
| Field | Type | Description |
|---|---|---|
company_id |
u64 |
Target company |
employee |
Address |
Employee Ed25519 public key |
commitment |
BytesN<32> |
Poseidon(salary, blinding) — 32 bytes |
| Returns | () |
void |
Behavior:
- Loads
CompanyInfoforcompany_id; panics with"Company not found"if missing. - Calls
info.admin.require_auth(). - Stores
commitmentunder(company_id, employee).
Errors:
panic!("Company not found")— company does not exist
| Field | Type | Description |
|---|---|---|
company_id |
u64 |
Target company |
employee |
Address |
Employee to remove |
| Returns | () |
void |
Behavior:
- Loads
CompanyInfo; panics if missing. - Requires admin auth.
- Hard-deletes the employee record.
Errors:
panic!("Company not found")
| Field | Type | Description |
|---|---|---|
company_id |
u64 |
Target company |
employee |
Address |
Employee whose commitment to update |
new_commitment |
BytesN<32> |
New Poseidon commitment |
| Returns | () |
void |
Behavior:
- Loads
CompanyInfo; panics if missing. - Requires admin auth.
- Checks employee exists via
storage().has(); panics"Employee not found"if absent. - Overwrites commitment in storage.
Errors:
panic!("Company not found")panic!("Employee not found")
| Field | Type | Description |
|---|---|---|
company_id |
u64 |
Target company |
| Returns | CompanyInfo |
See CompanyInfo |
Behavior: Read-only. Returns CompanyInfo { admin: Address, treasury: Address }.
Errors: panic!("Company not found")
| Field | Type | Description |
|---|---|---|
company_id |
u64 |
Target company |
employee |
Address |
Employee |
| Returns | BytesN<32> |
Stored commitment (32 bytes) |
Behavior: Read-only. Returns the raw commitment bytes stored for the employee.
Errors: panic!("Employee not found")
Source: contracts/salary_commitment/src/lib.rs
Contract ID: deployed as salary_commitment.wasm
15 public entrypoints: init, admin, operator, store, update, rotate, status, history, get, has, nullifier (record + check), compute, verify.
| Field | Type | Description |
|---|---|---|
admin |
Address |
HR admin for this contract |
| Returns | () |
void |
Behavior: One-time initialization. Sets the HR admin address that authorizes all commitment writes.
Errors: panic!("Already initialized")
| Field | Type | Description |
|---|---|---|
operator |
Address |
Address delegated to record nullifiers |
| Returns | () |
void |
Behavior: Only callable by the HR admin. Allows a separate address (typically the Payroll contract) to call record_nullifier.
Errors: panic!("Not initialized") — if admin not yet set.
| Field | Type | Description |
|---|---|---|
| Returns | Address |
The stored HR admin address |
Behavior: Read-only.
Errors: panic!("Not initialized")
| Field | Type | Description |
|---|---|---|
| Returns | Option<Address> |
Some(operator) or None |
Behavior: Read-only. Returns None if no operator has been set.
Errors: None.
| Field | Type | Description |
|---|---|---|
employee |
Address |
Employee address |
commitment |
BytesN<32> |
Poseidon(salary, blinding) — 32 bytes |
| Returns | SalaryCommitment |
See SalaryCommitment |
Behavior:
- Requires HR admin auth.
- Creates a new
SalaryCommitmentwithversion = 1,revoked = false, timestamps set to current ledger time. - Emits
(Symbol("CommitmentUpdated"), employee_Address) → (commitment,)event.
Errors:
panic!("Not initialized")— admin not set
| Field | Type | Description |
|---|---|---|
employee |
Address |
Employee address |
new_commitment |
BytesN<32> |
New Poseidon commitment |
| Returns | SalaryCommitment |
Updated commitment record |
Behavior:
- Requires HR admin auth.
- Archives the current commitment to
CommitmentHistory. - Creates new record with incremented
version,revoked = false. - Emits
(Symbol("CommitmentUpdated"), employee) → (new_commitment,)event.
Errors:
panic!("Commitment not found")— no existing commitment for employeepanic!("Not initialized")
| Field | Type | Description |
|---|---|---|
employee |
Address |
Employee address |
new_commitment |
BytesN<32> |
New Poseidon commitment |
| Returns | SalaryCommitment |
New active commitment |
Behavior:
- Archives the current commitment to history.
- Marks the existing record as
revoked = true(cannot be used in future proofs). - Stores the new commitment as active via
store_commitment. - Emits
(Symbol("CommitmentRotated"), employee) → (old_commitment, new_commitment)event.
Errors:
panic!("Commitment not found")panic!("Not initialized")
| Field | Type | Description |
|---|---|---|
employee |
Address |
Employee |
| Returns | bool |
true if active and not revoked |
Behavior: Read-only. Returns false if no commitment exists or the existing one has revoked = true.
Errors: None.
| Field | Type | Description |
|---|---|---|
employee |
Address |
Employee |
| Returns | Vec<CommitmentSnapshot> |
Ordered history |
Behavior: Returns archived snapshots in order (oldest first). Empty vec if no rotations have occurred.
Errors: None.
| Field | Type | Description |
|---|---|---|
employee |
Address |
Employee |
| Returns | SalaryCommitment |
Active commitment record |
Behavior: Read-only.
Errors: panic!("Commitment not found")
| Field | Type | Description |
|---|---|---|
employee |
Address |
Employee |
| Returns | bool |
Whether a commitment record exists |
Behavior: Read-only. Returns false if no commitment has ever been stored.
Errors: None.
| Field | Type | Description |
|---|---|---|
nullifier |
BytesN<32> |
Unique proof nullifier |
| Returns | () |
void |
Behavior:
- Requires HR admin OR payroll operator auth.
- Checks nullifier hasn't been used; panics if already recorded.
- Stores
PaymentNullifier { nullifier, used_at: current_timestamp }.
Errors:
panic!("Nullifier already used")panic!("Not initialized")
| Field | Type | Description |
|---|---|---|
nullifier |
BytesN<32> |
Nullifier bytes |
| Returns | bool |
Whether nullifier exists |
Behavior: Read-only.
Errors: None.
| Field | Type | Description |
|---|---|---|
salary |
u64 |
Salary amount |
blinding_factor |
BytesN<32> |
32-byte blinding factor |
| Returns | BytesN<32> |
SHA-256(salary ‖ blinding) hash |
Behavior:
- Encodes
salaryas 8 bytes little-endian. - Concatenates with 32-byte blinding factor.
- Returns SHA-256 hash.
- Note: Current on-chain implementation uses SHA-256. Production will migrate to Poseidon when Soroban host functions support it (CAP-0075).
Errors: None.
| Field | Type | Description |
|---|---|---|
employee |
Address |
Employee |
claimed_salary |
u64 |
Claimed salary amount |
blinding_factor |
BytesN<32> |
Blinding factor |
| Returns | bool |
true if commitment matches AND active |
Behavior: Retrieves stored commitment, recomputes hash from claimed salary + blinding, and checks equality AND that the stored commitment is not revoked.
Errors: panic!("Commitment not found")
Source: contracts/proof_verifier/src/lib.rs
Contract ID: deployed as proof_verifier.wasm
| Field | Type | Description |
|---|---|---|
admin |
Address |
Verifier admin |
| Returns | () |
void |
Behavior: One-time initialization.
Errors: panic!("Already initialized")
| Field | Type | Description |
|---|---|---|
| Returns | Address |
Stored admin address |
Errors: panic!("Not initialized")
| Field | Type | Description |
|---|---|---|
vk |
VerificationKey |
Groth16 verification key |
| Returns | () |
void |
Behavior: One-time setup. Requires admin auth. Stores the verification key.
Errors:
panic!("Not initialized")panic!("Verifier already initialized")
| Field | Type | Description |
|---|---|---|
| Returns | VerificationKey |
Stored VK |
Errors: panic!("Verifier not initialized")
| Field | Type | Description |
|---|---|---|
proof |
Groth16Proof |
Structured proof with A, B, C |
public_inputs |
Vec<BytesN<32>> |
Public inputs (commitment, amount, etc.) |
| Returns | bool |
true if proof is valid |
Behavior: Packs Groth16Proof into 256-byte buffer then delegates to verify_payment_proof. See 4.4 Proof Data for serialization layout.
Errors: None (returns false on any verification failure).
| Field | Type | Description |
|---|---|---|
proof |
BytesN<256> |
Flat 256-byte packed proof |
public_inputs |
Vec<BytesN<32>> |
Public inputs (32 bytes each) |
| Returns | bool |
true if proof is valid |
Behavior:
- Loads stored VK.
- Checks
public_inputs.len() + 1 == vk.ic.len()— returnsfalseif mismatch. - Delegates to Groth16 pairing check.
⚠️ Current implementation:simulated_verify_groth16always returnstrue. Production must replace with real BN254 pairing verification.
Errors: None (returns false on any failure).
Source: contracts/payment_executor/src/lib.rs
Contract ID: deployed as payment_executor.wasm
11 public entrypoints: initialize, admin, pause-manager, period lifecycle (create/close/get), payment (single/batch), payment query (get/is_paid), total.
| Field | Type | Description |
|---|---|---|
addresses |
ContractAddresses |
Addresses of registry, commitment, verifier, token contracts |
| Returns | () |
void |
Behavior: One-time initialization. Stores dependent contract addresses.
Errors: panic!("Already initialized")
| Field | Type | Description |
|---|---|---|
admin |
Address |
Executor-level admin |
| Returns | () |
void |
Behavior: One-time, protected by caller auth.
Errors: panic!("Executor admin already set")
| Field | Type | Description |
|---|---|---|
pause_manager |
Address |
PauseManager contract |
| Returns | () |
void |
Behavior: Only executor admin may call.
Errors: panic!("Executor admin not set")
| Field | Type | Description |
|---|---|---|
company_id |
u64 |
Target company |
| Returns | Result<PayrollPeriod, PaymentError> |
Created period or error |
Behavior:
- Loads registry, requires company admin auth.
- Period IDs are sequential per company (starting at
1). - Only one period per company can be open at a time — but check is implicit (duplicate ID returns
PeriodAlreadyExists). - Emits
(Symbol("PeriodCreated"), company_id) → (period_id,)event.
Errors:
Err(PaymentError::PeriodAlreadyExists)— if the next sequential ID somehow already exists (race condition guard)
| Field | Type | Description |
|---|---|---|
company_id |
u64 |
Target company |
period_id |
u32 |
Period to close |
| Returns | Result<PayrollPeriod, PaymentError> |
Closed period |
Behavior:
- Requires company admin auth.
- Sets
closed = true, recordsend_ledger. - Emits
(Symbol("PeriodClosed"), company_id) → (period_id,)event.
Errors:
Err(PaymentError::PeriodNotFound)Err(PaymentError::PeriodClosed)— period already closed
| Field | Type | Description |
|---|---|---|
company_id |
u64 |
Target company |
period_id |
u32 |
Period ID |
| Returns | Option<PayrollPeriod> |
Period or None |
Behavior: Read-only.
Errors: None.
| Field | Type | Description |
|---|---|---|
company_id |
u64 |
Target company |
employee |
Address |
Employee receiving payment |
amount |
i128 |
Payment amount (in token units) |
proof_a |
BytesN<64> |
Groth16 proof A (G1 point) |
proof_b |
BytesN<128> |
Groth16 proof B (G2 point) |
proof_c |
BytesN<64> |
Groth16 proof C (G1 point) |
nullifier |
BytesN<32> |
Unique payment nullifier |
period |
u32 |
Payroll period ID |
| Returns | Result<PaymentRecord, PaymentError> |
Payment record or error |
Behavior (in order):
- Pause check: If
PauseManageris configured, checksis_paused(); panics"Payroll is paused"if paused. - Period validation: Period must exist and be open (
!closed). - Nullifier check: Reject if nullifier
ProofAlreadyUsed. - Double-payment check: Reject if employee already paid in this period (
AlreadyPaid). - Commitment retrieval: Fetches commitment from
SalaryCommitmentcontract. - Company info: Fetches company metadata from
PayrollRegistry. - Admin auth: Requires company
admin.require_auth(). - Proof verification: Constructs public inputs as
[commitment, amount_as_public_input], callsProofVerifier.verify(). - Token transfer:
token.transfer(company.treasury, employee, amount). - State recording: Stores payment record, marks nullifier used, increments total paid + period payment count.
- Event: Emits
(Symbol("PayrollProcessed"), company_id) → (employee, amount, period).
Errors:
Err(PaymentError::PeriodNotFound)— period does not existErr(PaymentError::PeriodClosed)— period is closedErr(PaymentError::ProofAlreadyUsed)— nullifier already recordedErr(PaymentError::AlreadyPaid)— employee already paid in this periodpanic!("Invalid payment proof")— Groth16 verification failedpanic!("Payroll is paused")— pause manager is active and pausedpanic!("Company not found")— registry lookup failure (propagated)
| Field | Type | Description |
|---|---|---|
company_id |
u64 |
Target company |
employees |
Vec<Address> |
Employee addresses |
amounts |
Vec<i128> |
Payment amounts (same length) |
proofs_a |
Vec<BytesN<64>> |
G1 proof A components |
proofs_b |
Vec<BytesN<128>> |
G2 proof B components |
proofs_c |
Vec<BytesN<64>> |
G1 proof C components |
nullifiers |
Vec<BytesN<32>> |
Unique nullifiers per payment |
period |
u32 |
Payroll period ID |
| Returns | Result<Vec<PaymentRecord>, PaymentError> |
All payment records or first error |
Behavior:
- All input vectors must have identical length; returns
Err(ArrayLengthMismatch)otherwise. - Iterates sequentially, calling
execute_paymentfor each employee. - First failure aborts the entire batch.
| Field | Type | Description |
|---|---|---|
employee |
Address |
Employee address |
period |
u32 |
Payroll period ID |
| Returns | PaymentRecord |
Stored payment record |
Errors: panic!("Payment not found")
| Field | Type | Description |
|---|---|---|
employee |
Address |
Employee address |
period |
u32 |
Payroll period ID |
| Returns | bool |
Whether payment exists |
Errors: None.
| Field | Type | Description |
|---|---|---|
company_id |
u64 |
Target company |
| Returns | i128 |
Cumulative amount paid |
Errors: None (returns 0 for unknown companies).
Source: contracts/payroll/src/lib.rs
Contract ID: deployed as payroll.wasm
| Field | Type | Description |
|---|---|---|
admin |
Address |
Payroll admin |
token |
Address |
Token contract address |
verifier |
Address |
ProofVerifier address |
commitment |
Address |
SalaryCommitment addr |
treasury |
Address |
Treasury address |
| Returns | () |
void |
Errors: panic!("Already initialized")
| Field | Type | Description |
|---|---|---|
pause_manager |
Address |
PauseManager contract address |
| Returns | () |
void |
Errors: panic!("Not initialized") then admin auth failure.
| Field | Type | Description |
|---|---|---|
| Returns | Vec<Address> |
Assets currently enabled for this payroll contract |
Errors: None. The initialized token is included by default; repeated allowlist updates do not create duplicates.
| Field | Type | Description |
|---|---|---|
_from |
Address |
(unused) |
_amount |
i128 |
(unused) |
| Returns | () |
void |
Behavior: Placeholder — no-op in current implementation.
| Field | Type | Description |
|---|---|---|
proofs |
Vec<BytesN<256>> |
Flat 256-byte packed proofs |
amounts |
Vec<i128> |
Payment amounts |
employees |
Vec<Address> |
Employee addresses |
expected_total_spend |
i128 |
Admin-declared total (must equal sum) |
| Returns | () |
void |
Behavior (in order):
- Length check:
proofs.len() == amounts.len() == employees.len(). A missing proof panics"Missing payroll proof: one proof is required per payment"; other mismatches panic"Array length mismatch". - Batch size limit:
proofs.len() <= 50(constantMAX_BATCH). Panics"Batch too large". - Spend authorization: Sums all amounts, compares to
expected_total_spend. Panics on mismatch. - Pause check: If PauseManager configured, checks
is_paused(); panics"Payroll is paused"if paused. - Admin auth: Calls
addrs.admin.require_auth(). - Per-employee loop:
a. Retrieve commitment from
SalaryCommitmentcontract. Panics"Commitment not found"if missing. b. Construct public inputs:[commitment, nullifier, recipient_hash]. c. Verify proof viaProofVerifier.verify_payment_proof(). Panics"Invalid payment proof"on failure. d. Record nullifier viaSalaryCommitment.record_nullifier(). Panics"Nullifier already used"on replay. e. Transfer tokens:token.transfer(treasury, employee, amount). f. Emit event:(symbol_short!("payroll"), Symbol("payment_executed")) → (employee, amount).
[index_lsb, index_msb, 0, ..., 0]), not from the proof itself. Production must use a proper cryptographic nullifier.
Errors (all panic!, not Result):
panic!("Array length mismatch")panic!("Missing payroll proof: one proof is required per payment")panic!("Batch too large")panic!("Expected spend mismatch: authorised X but batch totals Y")panic!("Payroll is paused")panic!("Invalid payment proof for employee N")panic!("Nullifier already used")panic!("Commitment not found")
Source: contracts/audit_module/src/lib.rs
Contract ID: deployed as audit_module.wasm
| Field | Type | Description |
|---|---|---|
auditor |
Address |
Auditor address |
expiration_ledger |
u32 |
Expiration ledger seq |
| Returns | BytesN<32> |
SHA-256 derived view key |
Errors: None.
| Field | Type | Description |
|---|---|---|
auditor |
Address |
Auditor |
| Returns | bool |
Key exists and not expired |
Errors: None.
| Field | Type | Description |
|---|---|---|
admin |
Address |
Key granter |
auditor |
Address |
Auditor |
| Returns | Result<(), AuditError> |
void or error |
Errors:
Err(AuditError::KeyNotFound)Err(AuditError::NotKeyGranter)— caller is not the granter
| Field | Type | Description |
|---|---|---|
auditor |
Address |
Auditor |
| Returns | Result<ViewKeyRecord, AuditError> |
Record or error |
Errors: Err(AuditError::KeyNotFound)
| Field | Type | Description |
|---|---|---|
auditor |
Address |
Auditor |
stored_commitment |
BytesN<32> |
Commitment from employee |
claimed_amount |
i128 |
Claimed salary amount |
blinding_factor |
BytesN<32> |
Blinding factor |
scope |
AuditScope |
Access scope |
| Returns | Result<bool, AuditError> |
Match result or error |
Behavior: Computes keyed commitments and compares. Records audit log entry.
Errors:
Err(AuditError::KeyNotFound)Err(AuditError::KeyExpired)Err(AuditError::InsufficientScope)— scope isAggregateOnlyErr(AuditError::CommitmentMismatch)— hash does not match
| Field | Type | Description |
|---|---|---|
auditor |
Address |
Auditor |
supplied_key |
BytesN<32> |
The purported view key |
stored_commitment |
BytesN<32> |
Commitment from employee |
claimed_amount |
i128 |
Claimed salary amount |
blinding_factor |
BytesN<32> |
Blinding factor |
scope |
AuditScope |
Access scope |
| Returns | Result<bool, AuditError> |
Match result or error |
Errors: Same as verify_commitment_with_key plus:
Err(AuditError::InvalidViewKey)— supplied key does not match stored key
| Field | Type | Description |
|---|---|---|
auditor |
Address |
Auditor |
company_id |
Symbol |
Company symbol |
period_start |
u64 |
Start timestamp |
period_end |
u64 |
End timestamp |
| Returns | Result<AuditReport, AuditError> |
Report or error |
Errors:
Err(AuditError::KeyNotFound)Err(AuditError::KeyExpired)
| Method | Parameters | Returns |
|---|---|---|
query_by_company |
company_id: Symbol |
AuditQueryResult |
query_by_employee |
company_id: Symbol, employee: Address |
AuditQueryResult |
query_by_period |
company_id: Symbol, period_start: u64, period_end: u64 |
AuditQueryResult |
get_audit_log_count |
company_id: Symbol |
u32 |
All query methods are read-only with no errors.
record_audit_log helper hardcodes company_id = Symbol("default") regardless of the company being queried. This means query_by_company, query_by_employee, and query_by_period all return the same audit log entries regardless of the company_id argument. This will be addressed in a future release to scope audit logs per-company.
Source: contracts/pause_manager/src/lib.rs
Contract ID: deployed as pause_manager.wasm
| Method | Parameters | Returns | Access Control |
|---|---|---|---|
initialize |
operator: Address |
() |
None (one-time) |
pause |
— | () |
require_auth(operator) |
unpause |
— | () |
require_auth(operator) |
is_paused |
— | bool |
Public |
set_operator |
new_operator: Address |
() |
require_auth(operator) |
Errors:
panic!("Already initialized")—initializecalled twicepanic!("Not initialized")—pause/unpause/set_operatorbeforeinitialize
Source: contracts/token/src/lib.rs
| Method | Parameters | Returns | Notes |
|---|---|---|---|
initialize |
_admin: Address, _decimal: u32, _name: String, _symbol: String |
() |
No-op mock |
mint |
to: Address, amount: i128 |
() |
Increases balance |
balance |
id: Address |
i128 |
Returns stored balance |
transfer |
from: Address, to: Address, amount: i128 |
() |
Transfers; panics on insufficient balance |
| Field | Type | Description |
|---|---|---|
admin |
Address |
Company admin (auth key) |
treasury |
Address |
Token source address |
Storage: Keyed by DataKey::Company(u64) in persistent storage.
| Field | Type | Description |
|---|---|---|
commitment |
BytesN<32> |
Poseidon(salary, blinding_factor) |
created_at |
u64 |
Ledger timestamp at creation |
updated_at |
u64 |
Ledger timestamp at last update |
version |
u32 |
Monotonically increasing version |
revoked |
bool |
Whether commitment has been rotated |
Storage: Keyed by DataKey::Commitment(Address).
| Field | Type | Description |
|---|---|---|
commitment |
BytesN<32> |
Previously active commitment |
version |
u32 |
Version at time of rotation |
rotated_at |
u64 |
Timestamp of rotation |
Storage: Keyed by DataKey::CommitmentHistory(Address, u32).
| Field | Type | Description |
|---|---|---|
nullifier |
BytesN<32> |
Unique nullifier bytes |
used_at |
u64 |
Ledger timestamp |
Storage: Keyed by DataKey::Nullifier(BytesN<32>).
| Field | Type | Description |
|---|---|---|
company_id |
u64 |
Company ID |
employee |
Address |
Employee recipient |
proof_hash |
BytesN<32> |
Nullifier (used as proof hash) |
timestamp |
u64 |
Ledger timestamp of payment |
period |
u32 |
Payroll period ID |
Storage: Keyed by DataKey::Payment(Address, u32).
| Field | Type | Description |
|---|---|---|
period_id |
u32 |
Sequential period ID (per company) |
company_id |
u64 |
Company ID |
start_ledger |
u32 |
Ledger sequence when opened |
end_ledger |
u32 |
Ledger sequence when closed (0=open) |
created_at |
u64 |
Unix timestamp at creation |
closed |
bool |
Whether period is closed |
payment_count |
u32 |
Number of payments in this period |
Storage: Keyed by DataKey::Period(u64, u32).
| Field | Type | Description |
|---|---|---|
a |
BytesN<64> |
G1 point π_A (x ‖ y) |
b |
BytesN<128> |
G2 point π_B (x0 ‖ x1 ‖ y0 ‖ y1) |
c |
BytesN<64> |
G1 point π_C (x ‖ y) |
Flat encoding (256 bytes):
offset 0..64: a (G1)
offset 64..192: b (G2)
offset 192..256: c (G1)
| Field | Type | Description |
|---|---|---|
alpha |
BytesN<64> |
G1 point α |
beta |
BytesN<128> |
G2 point β |
gamma |
BytesN<128> |
G2 point γ |
delta |
BytesN<128> |
G2 point δ |
ic |
Vec<BytesN<64>> |
G1 elements for public inputs |
Note: ic.len() must equal public_inputs.len() + 1 for verification to succeed.
| Field | Type | Description |
|---|---|---|
registry |
Address |
PayrollRegistry contract |
commitment |
Address |
SalaryCommitment contract |
verifier |
Address |
ProofVerifier contract |
token |
Address |
Token contract |
| Field | Type | Description |
|---|---|---|
admin |
Address |
Payroll contract admin |
token |
Address |
Token contract |
verifier |
Address |
ProofVerifier contract |
commitment |
Address |
SalaryCommitment contract |
treasury |
Address |
Treasury address |
| Field | Type | Description |
|---|---|---|
key_bytes |
BytesN<32> |
Derived view key |
expiration_ledger |
u32 |
Expiration ledger sequence |
granted_by |
Address |
Admin who granted the key |
| Variant | Value | Description |
|---|---|---|
FullCompany |
0 |
Full access to company data |
TimeRange |
1 |
Access within a time range |
EmployeeList |
2 |
Access to specific employees |
AggregateOnly |
3 |
Aggregate statistics only |
| Field | Type | Description |
|---|---|---|
auditor |
Address |
Auditor address |
company_id |
Symbol |
Company symbol |
scope |
AuditScope |
Access scope used |
timestamp |
u64 |
Ledger timestamp |
matched |
bool |
Verification result |
| Field | Type | Description |
|---|---|---|
company_id |
Symbol |
Company symbol |
total_employees |
u32 |
Employee count |
total_paid |
i128 |
Total paid |
period_start |
u64 |
Period start |
period_end |
u64 |
Period end |
verified |
bool |
Verification |
| Field | Type | Description |
|---|---|---|
entries |
Vec<AuditLogEntry> |
Matching logs |
| Aspect | Rule |
|---|---|
| On-chain | Soroban Address (20 bytes for AccountId, 32+1 for contract) |
| String | Stellar StrKey format: G... (Ed25519), C... (Contract) |
| SDK | Use stellar-strkey for encode/verify |
| Binary | 32-byte Ed25519 public key for accounts |
| Identifier | Type | Wire Encoding | Endianness | Range |
|---|---|---|---|---|
| Company ID | u64 |
XDR Uint64 |
Big-endian | 0 to 2^64-1 |
| Period ID | u32 |
XDR Uint32 |
Big-endian | 1 to 2^32-1 |
| Ledger seq | u32 |
XDR Uint32 |
Big-endian | Stellar ledger range |
| Timestamp | u64 |
XDR Uint64 |
Big-endian | Unix epoch seconds |
Commitments are exactly 32 bytes (BytesN<32> in Soroban, [u8; 32] in Rust).
preimage = salary.to_le_bytes() ++ blinding_factor[0..32]
output = SHA-256(preimage)
Where:
salaryis au64encoded as 8 bytes little-endianblinding_factoris 32 bytes
input_1 = Fr(salary) // BN254 scalar from u64
input_2 = Fr(blinding_factor) // BN254 scalar from LE bytes
output = Poseidon([input_1, input_2]) // circomlib-compatible width-3 sponge
Notes:
- Off-chain uses Poseidon for Circom compatibility.
- On-chain currently uses SHA-256 as a placeholder.
- Migration: When Soroban CAP-0075 lands, on-chain will switch to Poseidon to match circuit.
BytesN<32> — 32 raw bytes
- Wire format: XDR
BytesN<32>(32-byte opaque) - Hex string: 64 lowercase hex characters (e.g.,
a1b2c3...) - Conversion: Use
stellar-xdrfor XDR; standard hex encoding for display
| Field | Size | Content |
|---|---|---|
a |
64 bytes | G1 point: x[32] ‖ y[32] — big-endian field elements |
b |
128 bytes | G2 point: x0[32] ‖ x1[32] ‖ y0[32] ‖ y1[32] — BE |
c |
64 bytes | G1 point: x[32] ‖ y[32] — big-endian field elements |
offset 0..64: G1 π_A (a.x, a.y)
offset 64..192: G2 π_B (b.x0, b.x1, b.y0, b.y1)
offset 192..256: G1 π_C (c.x, c.y)
Used by ProofVerifier.verify_payment_proof(proof: BytesN<256>, ...).
| Aspect | Rule |
|---|---|
| Size | Exactly 32 bytes per BN254 scalar field element |
| Endianness | Big-endian (matching EVM/ethers convention) |
| Range | Must be < BN254_R = 21888242871839275222246405745257275088548364400416034343698204186575808495617 |
| Zero-pad | Left-padded with zeros to 32 bytes |
| Hex | 64 lowercase hex characters per element |
{
"pi_a": "<128 hex chars>",
"pi_b": "<256 hex chars>",
"pi_c": "<128 hex chars>",
"salary_commitment": "<64 hex chars>",
"payment_nullifier": "<64 hex chars>",
"recipient_hash": "<64 hex chars>"
}PaymentExecutor: [commitment, amount]
commitment:BytesN<32>— employee's salary commitmentamount:BytesN<32>— 16-byte big-endian amount in upper 16 bytes of 32-byte field element
Payroll (batch): [commitment, nullifier, recipient_hash]
commitment:BytesN<32>— employee's salary commitmentnullifier:BytesN<32>— derived from batch index (production should use proof-derived value)recipient_hash:BytesN<32>— reserved (currently[0u8; 32])
| Aspect | Rule |
|---|---|
| Type | BytesN<32> (exactly 32 bytes) |
| Uniqueness | MUST be globally unique per payment |
| Replay guard | PaymentExecutor checks DataKey::Nullifier(BytesN<32>) |
| Construction | Off-chain: derived from proof public inputs. On-chain batch: derived from batch index ( |
When an i128 amount is converted to a public input (BytesN<32>) in PaymentExecutor:
if amount < 0 → panic!("Amount must be non-negative")
let bytes[0..16] = 0x00 * 16
let bytes[16..32] = amount.to_be_bytes()
return bytes as BytesN<32>
This places the amount in the upper 16 bytes (big-endian) of a 32-byte field element, with the lower 16 bytes zeroed.
All events are published via env.events().publish(topic, payload).
| Contract | Event Topic | Payload |
|---|---|---|
SalaryCommitment |
(Symbol("CommitmentUpdated"), employee: Address) |
(commitment: BytesN<32>,) |
SalaryCommitment |
(Symbol("CommitmentRotated"), employee: Address) |
(old: BytesN<32>, new: BytesN<32>) |
PaymentExecutor |
(Symbol("PeriodCreated"), company_id: u64) |
(period_id: u32,) |
PaymentExecutor |
(Symbol("PeriodClosed"), company_id: u64) |
(period_id: u32,) |
PaymentExecutor |
(Symbol("PayrollProcessed"), company_id: u64) |
(employee: Address, amount: i128, period: u32) |
Payroll |
(symbol_short!("payroll"), Symbol("payment_executed")) |
(employee: Address, amount: i128) |
PauseManager |
(Symbol("PauseManager"), Symbol("paused")) |
() |
PauseManager |
(Symbol("PauseManager"), Symbol("unpaused")) |
() |
AuditModule |
(Symbol("AuditSuccessful"), auditor: Address) |
(scope: AuditScope, keyed_commitment: BytesN<32>) |
AuditModule |
(Symbol("AggregateAuditGenerated"), auditor: Address) |
(company_id: Symbol, period_start: u64, period_end: u64) |
| Contract | Panic Message | Trigger |
|---|---|---|
| PayrollRegistry | "Company not found" |
Unknown company_id |
| PayrollRegistry | "Employee not found" |
Unknown employee in company |
| SalaryCommitment | "Already initialized" |
Duplicate init_commitment_admin |
| SalaryCommitment | "Not initialized" |
Admin not set before admin-gated call |
| SalaryCommitment | "Commitment not found" |
get_commitment for unregistered employee |
| SalaryCommitment | "Nullifier already used" |
Duplicate nullifier |
| ProofVerifier | "Already initialized" |
Duplicate init_verifier_admin |
| ProofVerifier | "Not initialized" |
Admin not set |
| ProofVerifier | "Verifier already initialized" |
Duplicate initialize_verifier |
| ProofVerifier | "Verifier not initialized" |
VK not set before verification |
| PaymentExecutor | "Already initialized" |
Duplicate initialize |
| PaymentExecutor | "Executor admin already set" |
Duplicate set_executor_admin |
| PaymentExecutor | "Executor admin not set" |
set_pause_manager before admin set |
| PaymentExecutor | "Payroll is paused" |
PauseManager active and paused |
| PaymentExecutor | "Invalid payment proof" |
Groth16 verification failed |
| PaymentExecutor | "Payment not found" |
get_payment for unknown (employee, period) |
| PaymentExecutor | "Amount must be non-negative" |
Negative amount in execute_payment |
| Payroll | "Already initialized" |
Duplicate initialize |
| Payroll | "Not initialized" |
set_pause_manager before init |
| Payroll | "Array length mismatch" |
Mismatched proof/amount/employee arrays |
| Payroll | "Batch too large" |
>50 employees in batch |
| Payroll | "Expected spend mismatch: ..." |
Sum of amounts ≠ expected_total_spend |
| Payroll | "Payroll is paused" |
PauseManager active and paused |
| Payroll | "Invalid payment proof for employee N" |
Individual proof verification failed |
| Payroll | "Nullifier already used" |
Nullifier replay detected (propagated) |
| Payroll | "Commitment not found" |
Missing employee commitment (propagated) |
| PauseManager | "Already initialized" |
Duplicate initialize |
| PauseManager | "Not initialized" |
pause/unpause before init |
| Token | "Mint amount must be non-negative" |
Negative mint amount |
| Token | "Insufficient balance" |
Insufficient funds for transfer |
| Variant | Code | Description |
|---|---|---|
ProofAlreadyUsed |
1 | Nullifier already on-chain (replay prevention) |
ArrayLengthMismatch |
2 | Batch input vectors have different lengths |
AlreadyPaid |
3 | Employee already paid in this period |
PeriodNotFound |
4 | No period exists for (company, period_id) |
PeriodClosed |
5 | Period is closed; no new payments allowed |
PeriodAlreadyExists |
6 | Duplicate period creation attempt |
| Variant | Code | Description |
|---|---|---|
KeyNotFound |
1 | No view key stored for auditor |
WrongAuditor |
2 | Caller is not the designated auditor |
KeyExpired |
3 | ledger_sequence > expiration_ledger |
NotKeyGranter |
4 | Caller is not the admin that granted the key |
InsufficientScope |
5 | Scope insufficient for requested operation |
CommitmentMismatch |
6 | Hash does not match stored commitment |
InvalidViewKey |
7 | Supplied key does not match stored record |
| Component | Guarantee Level | Notes |
|---|---|---|
| Entrypoint signatures | Stable v0.1 | Names, parameters, and types are locked. New params added as optional trailing args only. |
| Data structures | Stable v0.1 | Field layout (names, types, order) fixed. New field appended to end only. |
| Event topics | Stable v0.1 | Topic pattern and payload shape guaranteed. New events added with new topic symbols. |
| Error messages | Unstable | panic!("...") strings may change. Use typed PaymentError/AuditError codes for stable error handling. |
| Storage layout | Internal | DataKey enum variants may be added but existing keys (Company, Employee, etc.) will not be removed or reinterpreted. |
A change is breaking if it requires SDK modifications to maintain correctness:
- Removing or renaming an entrypoint — breaking (major version bump).
- Changing parameter types or order — breaking.
- Changing return types — breaking.
- Changing event topic or payload — breaking.
- Adding a required parameter — breaking (new overloaded entrypoint the SDK must call conditionally).
- Adding an optional parameter — non-breaking if old call pattern still works.
- Adding a new entrypoint — non-breaking (SDK can ignore).
- Changing panic message strings — non-breaking (SDKs should not parse panic strings).
- Switching SHA-256 → Poseidon — source-compatible for SDK callers (both produce
BytesN<32>). Off-chain commitment generators must update algorithm. - Replacing simulated Groth16 with real verification — interface-compatible; SDK sees same
boolreturn.
| Feature | Current State | Target State | Expected Impact |
|---|---|---|---|
| On-chain commitment hash | SHA-256 | Poseidon (CAP-0075) | Commitment byte size same; SDK generators must switch algorithm |
| Proof verification | simulated_verify_groth16 (always true) |
Real BN254 pairing checks | SDK proofs must now pass real verification |
| Payroll nullifier derivation | Batch-index-based | Cryptographic proof-derived | SDKs must send proper nullifier |
SalaryCommitment compute_commitment |
SHA-256 | Poseidon host function | Same interface, different output bytes |
| Token (mock) | Placeholder Token |
SEP-41 token contract | Same transfer interface |
- Always use typed error codes (
PaymentError,AuditError) for error handling, never parse panic strings. - Always use
try_prefixed methods (e.g.,try_execute_payment) for fallible calls that returnResult. The non-tryvariants panic on error. - Validate proof byte lengths client-side before submission: A = 64, B = 128, C = 64, flat proof = 256.
- For batch operations, ensure all input vectors have identical lengths.
- Monitor events for
PayrollProcessedandCommitmentUpdatedfor off-chain indexing. - Pre-allocate company IDs with
register_companybefore adding employees.
For SDK implementations that generate commitments off-chain:
blinding = OsRng(64 bytes) % BN254_R → 32-byte LE field element
commitment = Poseidon(
Fr(salary),
Fr(blinding)
) → 32-byte LE field element
- Blinding factor: 64 bytes from CSPRNG, reduced modulo BN254 scalar field prime.
- Field encoding: Little-endian canonical representation (arkworks convention).
- Hash: Width-3 Poseidon sponge, circomlib-compatible parameters.
- Output: 32 bytes little-endian.
Payroll.batch_process_payroll()
├── SalaryCommitment.get_commitment(employee) → commitment_bytes
├── ProofVerifier.verify_payment_proof(proof, inputs) → bool
├── SalaryCommitment.record_nullifier(nullifier) → void
└── Token.transfer(treasury, employee, amount) → void
PaymentExecutor.execute_payment()
├── PayrollRegistry.get_company(company_id) → CompanyInfo
├── SalaryCommitment.get_commitment(employee) → SalaryCommitment
├── ProofVerifier.verify(proof, inputs) → bool
└── Token.transfer(treasury, employee, amount) → void