Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,28 @@ Horizon proof exists).

---

### 🔄 Upgrades & Governance

* Single-admin governance — one address (set at `initialize`) controls upgrades, migrations, and feature flags
* Contract version stored in persistent ledger — survives ledger entry expiry
* Feature flags allow toggling behaviours without a full WASM upgrade
* `ContractInitialized` and `ContractUpgraded` events let indexers detect which contract version produced any given document event

---

### 📦 Batch Operations

* `batch_register_documents` — register up to 20 documents in one transaction
* `batch_revoke_documents` — revoke up to 20 documents in one transaction

**Atomicity:** All documents succeed or none are written. If any item in the batch fails (e.g. duplicate hash, wrong issuer, already revoked), the entire call returns an error and no state is changed.

**Batch size limit:** Maximum 20 documents per call. Exceeding this returns `BatchTooLarge` (error code 7). Empty batches return `BatchEmpty` (error code 8).

**Fee implications:** A single transaction covers the entire batch regardless of size, making bulk operations significantly cheaper than individual calls. For best results, pre-validate document uniqueness and existence client-side before submitting to avoid wasted transaction fees on partial failures.

---

## 🧠 How It Works

1. Document is hashed (SHA256)
Expand Down Expand Up @@ -142,6 +164,68 @@ soroban contract deploy \

---

### Initialize After Deployment

After deploying, call `initialize` to set the admin address and record version 1 on-chain:

```bash
soroban contract invoke \
--id <CONTRACT_ID> \
--source <ADMIN_SECRET_KEY> \
--network testnet \
-- initialize \
--admin <ADMIN_ADDRESS>
```

---

### Upgrade Procedure

1. **Build the new WASM** and upload it to the ledger:

```bash
cargo build --target wasm32-unknown-unknown --release
soroban contract install \
--wasm target/wasm32-unknown-unknown/release/proofstell_contract.wasm \
--network testnet
# Note the returned WASM hash
```

2. **Call `upgrade`** with the new WASM hash:

```bash
soroban contract invoke \
--id <CONTRACT_ID> \
--source <ADMIN_SECRET_KEY> \
--network testnet \
-- upgrade \
--admin <ADMIN_ADDRESS> \
--new_wasm_hash <WASM_HASH>
```

3. **Call `migrate`** to apply any data transformations and bump the version:

```bash
soroban contract invoke \
--id <CONTRACT_ID> \
--source <ADMIN_SECRET_KEY> \
--network testnet \
-- migrate \
--admin <ADMIN_ADDRESS>
```

### Rollback Plan

Soroban contract upgrades are irreversible on-chain — there is no undo. To roll back:

1. Keep the previous WASM hash recorded before upgrading.
2. If the new version is broken, call `upgrade` again with the old WASM hash.
3. If the migration mutated storage in an incompatible way, a compensating migration must be written into the rolled-back WASM.

**Recommendation:** always test upgrades on testnet before applying to mainnet. See [docs/UPGRADE_GOVERNANCE.md](docs/UPGRADE_GOVERNANCE.md) for the full decision process.

---

## 🧪 Testing

```bash
Expand Down
95 changes: 95 additions & 0 deletions docs/UPGRADE_GOVERNANCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# ProofStell Contract Upgrade Governance

This document defines the decision process for upgrading the ProofStell Soroban contract, classifying change types, and authorizing governance actions.

---

## Change Classification

### Non-Breaking Changes (Safe to deploy without client migration)

- Adding new contract entry points
- Adding new event types
- Adding new error codes
- Adding new feature flags
- Internal refactors that preserve existing function signatures and storage layout

### Breaking Changes (Require coordinated client migration)

- Renaming or removing existing entry points
- Changing the type or field order of `DocumentRecord`, `DocumentInfo`, or any `#[contracttype]`
- Changing existing event topic strings or data fields
- Renumbering existing error codes
- Changing the storage key layout (`DataKey` variants)

Any breaking change must follow the full upgrade process below.

---

## Governance Roles

| Role | Responsibility |
|------|----------------|
| **Admin** | The address stored at `DataKey::Admin`. Authorized to call `upgrade`, `migrate`, and `set_feature_flag`. |
| **Maintainers** | Open-source contributors with merge rights on this repository. Propose and review changes. |
| **Community** | GitHub Issues and Discussions — the venue for raising concerns before any upgrade. |

---

## Upgrade Decision Process

### For Non-Breaking Changes

1. Open a PR with the change.
2. At least one maintainer reviews and approves.
3. Merge to `main`. Deploy at the next release window.

### For Breaking Changes

1. **Proposal** — Open a GitHub Issue labelled `breaking-change` describing:
- What is changing and why
- Which clients or indexers are affected
- The proposed migration window (minimum 2 weeks for mainnet)
2. **Discussion window** — At least 7 days for community feedback.
3. **PR review** — Two maintainer approvals required.
4. **Testnet dry-run** — Run the full upgrade procedure on testnet and verify:
- All existing tests pass against the new WASM
- `migrate` completes without error
- Document verification still works for pre-migration records
5. **Mainnet upgrade** — Admin calls `upgrade` then `migrate` on mainnet.
6. **Client migration notice** — Post a GitHub Discussion and update client documentation.

---

## Emergency Upgrade Procedure

For critical security fixes that cannot wait for the standard process:

1. Admin calls `upgrade` with the patched WASM on testnet immediately.
2. Verify no data corruption with a smoke test.
3. Admin calls `upgrade` on mainnet.
4. Open a GitHub Issue retroactively documenting the emergency change within 24 hours.
5. A follow-up PR with tests and documentation is required within 72 hours.

---

## Incompatible Storage Layout Changes

If a new version requires a `DocumentRecord` field to be added or removed:

1. The old storage key format must be read and transformed in `migrate`.
2. A new `DataKey` variant should be introduced for the new format; do not reuse old keys.
3. After migration completes, the old keys may be cleaned up in a subsequent release.
4. Tests must cover reading old-format records and verifying they are correctly transformed.

---

## Transferring Admin

To transfer governance to a new address, the current admin must call `initialize` is not re-callable — the admin address can only change by deploying a new contract or by the current admin upgrading to a contract version that supports admin transfer. Add an `transfer_admin(env, admin, new_admin)` entry point in a future version if this is needed.

---

## Audit Requirements

Before any mainnet upgrade that introduces new entry points or modifies existing logic, a security review is strongly recommended. See the [security review checklist](.github/SECURITY.md) if available.
121 changes: 121 additions & 0 deletions docs/superpowers/specs/2026-06-21-batch-operations-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
---
name: batch-operations-design
description: Design spec for batch_register_documents and batch_revoke_documents on the ProofStell Soroban contract
metadata:
type: project
---

# Batch Operations Design

**Issue:** #30 — Implement batch operations for bulk registration and revocation

## Problem

The contract only exposes single-document `register_document` and `revoke_document` entry points. Registering N documents requires N separate transactions, multiplying fees, latency, and the risk of partial on-chain state.

## Approach

Strict fail-fast atomicity. Both batch functions validate the full input upfront, then iterate — returning an error on the first failure. Because Soroban aborts and rolls back the entire transaction on any contract error, no partial state is ever written. This is the simplest model and exactly matches "all succeed or all fail."

Batch size is capped at **20 documents** to stay within Soroban instruction and storage operation limits. Empty batches are rejected to avoid ambiguous success responses.

## Data Model Changes

### New contracttype — `DocumentInfo`

```rust
#[contracttype]
pub struct DocumentInfo {
pub owner: Address,
pub document_hash: BytesN<32>,
}
```

Used as the element type for `batch_register_documents`. Groups owner and hash together so the vector is a single typed parameter (Soroban does not support tuple parameters in contract function signatures).

### New error codes

| Variant | Code | Description |
|----------------|------|------------------------------------------|
| `BatchTooLarge`| 7 | Input vector exceeds the 20-item limit |
| `BatchEmpty` | 8 | Input vector is empty |

These extend `ContractError` without renumbering existing codes.

## New Entry Points

### `batch_register_documents`

```rust
pub fn batch_register_documents(
env: Env,
issuer: Address,
documents: Vec<DocumentInfo>,
) -> Result<Vec<DocumentRecord>, ContractError>
```

- Calls `issuer.require_auth()` once for the whole batch.
- Returns `BatchEmpty` if `documents.len() == 0`.
- Returns `BatchTooLarge` if `documents.len() > 20`.
- Iterates in order; on any error (e.g. `AlreadyRegistered`) returns immediately.
- On full success: persists all records, emits one `DocumentRegistered` event per document, returns `Vec<DocumentRecord>`.

### `batch_revoke_documents`

```rust
pub fn batch_revoke_documents(
env: Env,
issuer: Address,
document_hashes: Vec<BytesN<32>>,
) -> Result<Vec<DocumentRecord>, ContractError>
```

- Calls `issuer.require_auth()` once for the whole batch.
- Returns `BatchEmpty` if `document_hashes.len() == 0`.
- Returns `BatchTooLarge` if `document_hashes.len() > 20`.
- Iterates in order; on any error (`DocumentNotFound`, `OnlyIssuerCanRevoke`, `AlreadyRevoked`) returns immediately.
- On full success: updates all records to `Revoked`, emits one `DocumentRevoked` event per document, returns `Vec<DocumentRecord>`.

## Error Handling

All existing per-document errors (`AlreadyRegistered`, `DocumentNotFound`, `OnlyIssuerCanRevoke`, `AlreadyRevoked`) propagate unchanged from within the batch loop. The caller receives the first error that occurred; no document index is embedded in the error (Soroban error codes are `u32` scalars — no payload). Clients should validate uniqueness and existence before submitting large batches to avoid wasted fees.

## Events

One event is emitted per document, identical in structure to the single-document events:
- `DocumentRegistered { issuer, owner, document_hash }` per registered document.
- `DocumentRevoked { issuer, document_hash }` per revoked document.

No batch-level event is emitted — downstream consumers already process individual document events and do not need a separate batch envelope.

## Tests

Located inline in `src/lib.rs` (matching existing test structure):

| Test | Scenario |
|------|----------|
| `batch_register_all_succeed` | 3 distinct documents register successfully, all returned Active |
| `batch_register_fails_on_duplicate` | 1 of 3 is already registered — entire batch fails, others not stored |
| `batch_register_rejects_empty` | empty vec → `BatchEmpty` |
| `batch_register_rejects_oversized` | 21 items → `BatchTooLarge` |
| `batch_revoke_all_succeed` | 3 registered documents revoked, all returned Revoked |
| `batch_revoke_fails_on_missing` | 1 of 3 not registered — entire batch fails |
| `batch_revoke_fails_on_wrong_issuer` | different issuer → `OnlyIssuerCanRevoke`, nothing revoked |
| `batch_revoke_fails_on_already_revoked` | 1 already revoked → `AlreadyRevoked`, nothing else revoked |
| `batch_revoke_rejects_empty` | empty vec → `BatchEmpty` |
| `batch_revoke_rejects_oversized` | 21 items → `BatchTooLarge` |

## README Changes

Add a **Batch Operations** section documenting:
- Function signatures and parameter types.
- The 20-document limit and the rationale (Soroban instruction budget).
- Atomicity guarantee and fee implications (one transaction regardless of batch size).
- Recommendation to pre-validate uniqueness/existence client-side.

## Files to Change

| File | Change |
|------|--------|
| `src/lib.rs` | Add `DocumentInfo`, `BatchTooLarge`/`BatchEmpty` errors, `batch_register_documents`, `batch_revoke_documents`, and all tests |
| `README.md` | Add Batch Operations section |
Loading
Loading