QuorumProof includes an automated backup system that creates daily encrypted snapshots of contract state and stores them in S3 for disaster recovery.
Daily Cron (2 AM UTC)
│
▼
GitHub Actions Workflow
│
├─→ Backup Script (backup.sh)
│ ├─→ Fetch contract state
│ ├─→ Encrypt with AES-256
│ └─→ Upload to S3
│
└─→ Verification Script (verify_snapshot.sh)
└─→ Validate backup integrity
Store these as GitHub secrets:
# GitHub Settings → Secrets and variables → Actions
BACKUP_S3_BUCKET=quorumproof-backups
BACKUP_ENCRYPTION_KEY=<strong-random-key>
CONTRACT_QUORUM_PROOF_TESTNET=<testnet-contract-id>
CONTRACT_QUORUM_PROOF_MAINNET=<mainnet-contract-id># Backup without encryption
./scripts/backup.sh
# Backup with encryption
export BACKUP_ENCRYPTION_KEY="your-encryption-key"
./scripts/backup.sh --encrypt
# Backup and upload to S3
./scripts/backup.sh --encrypt --upload quorumproof-backups./scripts/verify_snapshot.sh backups/daily/quorumproof-2026-05-29_14-30-00.jsonEach backup includes:
{
"backup_date": "2026-05-29_14-30-00",
"network": "testnet",
"contract_id": "CAAAAAAA...",
"credential_count": 42,
"slice_count": 5,
"backup_integrity": {
"checksum_algorithm": "sha256",
"data_checksum": "abc123def456...",
"metadata_checksum": "def789ghi012...",
"compressed": true,
"encrypted": true
},
"credentials": [
{
"id": 1,
"subject": "GXXXXXX...",
"issuer": "GXXXXXX...",
"credential_type": "MechanicalEngineeringDegree",
"metadata_hash": "0x1234...",
"revoked": false,
"expires_at": 1747569600
}
],
"slices": [
{
"id": 1,
"creator": "GXXXXXX...",
"attestors": ["GXXXXXX...", "GXXXXXX..."],
"threshold": 2
}
]
}Backups include SHA256 checksums for integrity verification:
# Verify backup checksum
./scripts/verify_backup_integrity.sh \
--backup backups/daily/quorumproof-2026-05-29.json.enc \
--checksum abc123def456...
# Output:
# ✓ Data checksum verified: abc123def456...
# ✓ Metadata checksum verified: def789ghi012...
# ✓ Backup integrity confirmed- Data Checksum: SHA256 hash of all credential and slice data
- Metadata Checksum: SHA256 hash of backup metadata (date, counts, etc.)
- Combined Checksum: HMAC-SHA256 using encryption key
The verification process validates:
- JSON structure is well-formed
- All required fields present
- Data checksums match stored values
- Credential count is accurate
- Slice count is accurate
- No data corruption detected
- Encryption key matches (if encrypted)
- Algorithm: AES-256-CBC with PBKDF2 key derivation
- Storage: GitHub Secrets (encrypted at rest)
- Rotation: Every 90 days or after suspected compromise
export BACKUP_ENCRYPTION_KEY="your-strong-key"
./scripts/backup.sh --encryptexport BACKUP_ENCRYPTION_KEY="your-strong-key"
./scripts/restore_from_backup.sh --backup backups/daily/quorumproof-2026-05-29.json.enc# Create bucket
aws s3 mb s3://quorumproof-backups
# Enable versioning
aws s3api put-bucket-versioning \
--bucket quorumproof-backups \
--versioning-configuration Status=Enabled
# Enable encryption
aws s3api put-bucket-encryption \
--bucket quorumproof-backups \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
}
}]
}'
# Set lifecycle policy (retain 90 days)
aws s3api put-bucket-lifecycle-configuration \
--bucket quorumproof-backups \
--lifecycle-configuration '{
"Rules": [{
"Id": "DeleteOldBackups",
"Status": "Enabled",
"Expiration": {"Days": 90},
"Filter": {"Prefix": "quorumproof/"}
}]
}'# List all backups
aws s3 ls s3://quorumproof-backups/quorumproof/ --recursive
# List testnet backups
aws s3 ls s3://quorumproof-backups/quorumproof/testnet/
# Download a backup
aws s3 cp s3://quorumproof-backups/quorumproof/testnet/quorumproof-2026-05-29.json.enc .# Restore from encrypted backup
export BACKUP_ENCRYPTION_KEY="your-encryption-key"
./scripts/restore_from_backup.sh \
--backup backups/daily/quorumproof-2026-05-29.json.enc \
--contract CAAAAAAA... \
--network testnet# Download backup from S3
aws s3 cp s3://quorumproof-backups/quorumproof/testnet/quorumproof-2026-05-29.json.enc .
# Restore
export BACKUP_ENCRYPTION_KEY="your-encryption-key"
./scripts/restore_from_backup.sh \
--backup quorumproof-2026-05-29.json.enc \
--contract CAAAAAAA... \
--network testnet# Verify backup structure
./scripts/verify_snapshot.sh backups/daily/quorumproof-2026-05-29.json
# Output:
# ✓ JSON is well-formed
# ✓ Credential count matches on-chain (42 == 42)
# ✓ Slice count matches on-chain (5 == 5)
# ✓ No missing credential IDsAfter restore, verify the contract state:
# Check credential count
soroban contract invoke \
--id CAAAAAAA... \
--network testnet \
-- get_credential_count
# Check slice count
soroban contract invoke \
--id CAAAAAAA... \
--network testnet \
-- get_slice_count
# Spot-check a credential
soroban contract invoke \
--id CAAAAAAA... \
--network testnet \
-- get_credential --credential-id 1In addition to the off-chain S3 backup pipeline above, the quorum_proof
contract itself supports lightweight on-chain snapshots of its aggregate
counters (credential, slice, and dispute counts). These act as a fast
integrity checkpoint and a way to recover the aggregate counters without
waiting for an off-chain restore, and they are what create_state_snapshot
records get read from by tooling that wants to cross-check an off-chain
backup against on-chain state at the time it was taken.
soroban contract invoke \
--id CAAAAAAA... \
--network testnet \
-- create_state_snapshot \
--admin <ADMIN> \
--description "pre-upgrade checkpoint"
# returns the new snapshot_id, e.g. 7# List all snapshot IDs
soroban contract invoke --id CAAAAAAA... --network testnet -- list_snapshots
# Fetch a specific snapshot's recorded counts and hashes
soroban contract invoke --id CAAAAAAA... --network testnet \
-- get_snapshot --snapshot_id 7soroban contract invoke \
--id CAAAAAAA... \
--network testnet \
-- restore_from_snapshot \
--admin <ADMIN> \
--snapshot_id 7Before restoring, the contract:
- Confirms the snapshot exists (
SnapshotNotFoundif not). - Confirms the snapshot's
state_versionmatches the contract's current schema version, refusing to restore snapshots taken under an incompatible schema. - Recomputes the snapshot's hashes from its own recorded counts and
compares them against the hashes stored at snapshot time
(
SnapshotCorruptedif they don't match) — this catches a snapshot record that was corrupted or tampered with after creation.
On success, the contract resets CredentialCount, SliceCount, and
DisputeCount to the values captured in the snapshot, and records the
restored snapshot ID (queryable via get_last_restored_snapshot) for audit
purposes.
Scope note: restoring only resets the aggregate counters, not every individual credential/slice/dispute record — rewriting the full data set in a single on-chain transaction would exceed Soroban's per-transaction resource limits for any registry of meaningful size. Full record-level recovery goes through the off-chain Restore Procedure above, which replays individual records from the S3/local JSON backup. Use the on-chain snapshot to confirm the counters an off-chain restore should converge to.
The .github/workflows/backup.yml workflow runs daily at 2 AM UTC:
- Backs up testnet contract
- Backs up mainnet contract
- Encrypts both backups
- Uploads to S3
- Verifies integrity
- Stores artifacts (90-day retention)
# Trigger backup workflow manually
gh workflow run backup.ymlCheck backup status in GitHub Actions:
# List recent backup runs
gh run list --workflow backup.yml --limit 10
# View latest backup logs
gh run view --log $(gh run list --workflow backup.yml --limit 1 --json databaseId -q '.[0].databaseId')-
Pause the contract (if still accessible)
soroban contract invoke --id CAAAAAAA... -- pause --admin <ADMIN>
-
Redeploy contract (if necessary)
./scripts/deploy_testnet.sh
-
Restore from backup
export BACKUP_ENCRYPTION_KEY="your-key" ./scripts/restore_from_backup.sh \ --backup backups/daily/quorumproof-2026-05-29.json.enc \ --contract <NEW_CONTRACT_ID> \ --network testnet
-
Verify restoration
cargo test -
Unpause contract
soroban contract invoke --id <NEW_CONTRACT_ID> -- unpause --admin <ADMIN>
| Backup Type | Retention | Storage | Frequency |
|---|---|---|---|
| Local backups | 7 days | backups/daily/ |
Daily |
| S3 backups | 90 days | AWS S3 | Daily |
| GitHub artifacts | 90 days | GitHub Actions | Daily |
| Snapshots | Permanent | Blockchain | Continuous |
# Set environment variable
export CONTRACT_QUORUM_PROOF=CAAAAAAA...
./scripts/backup.sh# Set encryption key
export BACKUP_ENCRYPTION_KEY="your-strong-key"
./scripts/backup.sh --encrypt- Verify AWS credentials are configured
- Check S3 bucket permissions
- Verify IAM user has
s3:PutObjectpermission
- Verify backup file is not corrupted
- Check encryption key is correct
- Verify backup was created with same contract
- Test restores regularly — Run restore drills monthly on testnet
- Rotate encryption keys — Every 90 days or after suspected compromise
- Monitor backup status — Check GitHub Actions workflow runs daily
- Verify S3 backups — Spot-check S3 backups monthly
- Document procedures — Keep runbook updated with current contract IDs
- Secure encryption keys — Store in GitHub Secrets, never in code
- Test failover — Verify RPC failover endpoint works
- Disaster Recovery — Full recovery procedures
- Audit Log Format — Backup contents reference
- Deployment Guide — Contract deployment