Skip to content
Closed
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
147 changes: 147 additions & 0 deletions solutions/LP-0013.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Solution: LP-0013 — Token program authorities

**Submitted by:** ego-errante

## Summary

This solution adds a rotatable mint authority model to the LEZ Token program. The implementation is built around `lez-approval` — a new agnostic single-admin approval crate that fulfils [RFP-001](https://github.com/logos-co/rfp/blob/master/RFPs/RFP-001-admin-authority-lib.md) — and exposes three new instructions (`NewFungibleDefinitionWithAuthority`, `MintWithAuthority`, `RotateAuthority`, `RevokeAuthority`) layered additively over the existing Token surface. Revocation is terminal (`Authority::renounced()` is a permanent sentinel), so fixed-supply tokens are expressible as "mint everything, then revoke."

## Repository

- **Repo:** <https://github.com/ego-errante/logos-execution-zone>
- **Branch:** `lp-0013-token-authorities`
- **Commit:** [`9e222602`](https://github.com/ego-errante/logos-execution-zone/commit/9e222602)
- **Key files:**
- `lez-approval/src/lib.rs` — RFP-001 agnostic approval library (`Authority`, `ApprovalError`)
- `programs/token/core/src/lib.rs` — `Instruction` enum extensions and the canonical `Authority` field on `TokenDefinition::Fungible`
- `programs/token/src/{new_definition,mint,rotate}.rs` — handler implementations
- `program_methods/guest/src/bin/token.rs` — guest dispatcher
- `wallet/src/{program_facades,cli/programs}/token.rs` — wallet facade + CLI subcommands
- `examples/{fixed-supply,variable-supply}/` — two example integrations
- `integration_tests/tests/token_authority.rs` — end-to-end lifecycle tests
- `artifacts/token.idl.spel.json` — SPEL-emitted IDL (provenance, generated from `spel-sidecar/`)
- `artifacts/token.idl.json` — hand-authored canonical IDL (completeness, conforms to `SpelIdl`)
- `spel-sidecar/` — sidecar Cargo package outside the workspace that hosts the SPEL-shape mirror
- `docs/SPEL_STATUS.md` — disclosure of the IDL story (why a sidecar, the scaffold↔real mapping)
- `demo.sh` — clean-checkout end-to-end demo (rotate / revoke flow ends with balance 1500)

## Approach

### Architecture

The Token program already had `Mint`, `Transfer`, `NewFungibleDefinition`, etc. as variants of a single `Instruction` enum dispatched by `program_methods/guest/src/bin/token.rs`. We layer the authority model **additively**: three new instruction variants (`NewFungibleDefinitionWithAuthority`, `MintWithAuthority`, `RotateAuthority`, `RevokeAuthority`) coexist with the existing surface. The pre-existing `Mint` and `NewFungibleDefinition` paths are untouched, so naïve callers continue to work and the new variants are entirely opt-in for callers that want the gated lifecycle.

State-wise, the authority is recorded as a single new `authority: Authority` field on `TokenDefinition::Fungible`. We chose to store the authority **on** the definition rather than in a separate PDA (the Solana SPL pattern) because LEZ's per-account model already gives us atomic read-modify-write semantics within a single transaction — there is no concurrency benefit to splitting the field out, and a separate PDA would double the account count of every authority-gated instruction. The trade-off is that the `authority` field is a breaking Borsh-layout change for pre-existing `TokenDefinition::Fungible` accounts; this is documented in the `TokenDefinition` doc comment with the deferred `FungibleV2` / separate-PDA alternatives that would preserve in-flight schema compatibility if LEZ ever introduces such a guarantee.

### `lez-approval` as the RFP-001 deliverable

The agnostic approval library is implemented as a new top-level workspace crate, `lez-approval`. It exposes:

- `Authority(Option<AccountId>)` — a single-admin wrapper whose `None` state is terminal (the renounced sentinel). The `Option` is intentionally hidden behind constructor (`Authority::new`, `Authority::renounced`) and predicate (`Authority::is_renounced`) methods so callers cannot accidentally reach into the `None` state via pattern matching and revive a renounced authority.
- `ApprovalError { Unauthorized, Renounced }` — the two-variant error type that the panic-on-failure semantics surface as panic payloads. `Unauthorized` covers both "the wrong signer" and "the signer is not authorized at all"; `Renounced` covers all post-revocation rejections.
- A `gate` / `rotate` / `revoke` contract that the Token program calls through.

LEZ guest programs panic on failure (the prover catches the panic and rejects the transaction), so we follow the same convention: every authorization failure panics with an `ApprovalError` payload rather than returning a `Result`. This matches the surrounding code and avoids each handler having to write `match … panic!()` boilerplate. The library is generic — it depends only on `nssa_core::account::AccountId` and could be reused verbatim by any other LEZ program that wants single-admin gating (e.g. a freeze authority on the same Token program, or a config authority on a future on-chain module).

### Why the Logos stack

The single-admin authority model only earns its keep when the underlying execution layer makes it cheaply enforceable, atomic, and observably correct.

- **Trustless execution.** Because every state transition is proved in a RISC0 guest, a renounced authority is provably renounced — there is no off-chain admin key that someone could secretly retain to revive it. On a centralised alternative the "revoked" state would always be a social commitment; on LEZ it is a circuit-enforced invariant.
- **Censorship resistance.** Authority rotation is just another transaction in the LEZ mempool; the sequencer cannot selectively reject it without dropping the whole block. A centralised token-issuance API could refuse rotation requests from blocked accounts.
- **Atomicity at the right grain.** The post-state of `rotate_authority` either updates the definition account's `authority` field or panics; there is no intermediate state where the authority is `None` but the new admin is not yet recorded. LEZ's per-account read-modify-write semantics give us this for free, which is why we did not need a two-phase commit pattern.

A centralised token-issuance backend could replicate the API surface, but it could not give a wallet a proof that a token's mint authority has been provably revoked. That is the property the Logos stack uniquely enables for this use case.

### Alternatives considered

- **Separate authority PDA (Solana SPL pattern).** Rejected for cost (doubles per-transaction account count) and complexity (no concurrency benefit on LEZ). Documented as a future option in `TokenDefinition`'s doc comment if LEZ ever introduces in-flight Borsh-schema compatibility guarantees.
- **`Result`-returning handlers instead of panic-on-failure.** Rejected for consistency with the surrounding LEZ guest convention — every other handler in the Token program panics on failure. Mixing return styles would be a maintenance trap.
- **In-place mutation of `Instruction::Mint` to add an optional authority argument.** Rejected as a breaking change to the wire format that would have invalidated every existing caller. Additive variants are the correct LEZ idiom for this kind of evolution.
- **`#[lez_program]` directly on `program_methods/guest/src/bin/token.rs` to get a real-program SPEL IDL.** Attempted and abandoned — pulling `spel-framework` into the workspace causes a hard `nssa_core` v0.1.0 vs v0.2.0-rc3 dep-graph collision (`#[lez_program]` proc-macro emits path-literal references to whichever `nssa_core` cargo picked for the macro-invoking crate; one universe per macro invocation). The `[patch]`-to-local-path workaround is a coin flip due to feature-flag and API drift. Full disclosure in `docs/SPEL_STATUS.md`. We instead ship the IDL via a sidecar scaffold (`spel-sidecar/`) that runs SPEL out-of-workspace, plus a hand-authored canonical IDL — matching the bar set by the prior community submission ([PR #57](https://github.com/logos-co/lambda-prize/pull/57)).

## Success Criteria Checklist

### Functionality

- [x] **Variable-size Tokens through minting authority** — `NewFungibleDefinitionWithAuthority` sets the initial authority at definition creation; `MintWithAuthority` mints gated by the recorded authority; `RotateAuthority` and `RevokeAuthority` cover rotation and terminal revocation (renounced state is a permanent sentinel via `Authority::renounced()`).
- [x] **Two example integrations** — `examples/fixed-supply/` (mint everything, then revoke) and `examples/variable-supply/` (rotatable inflation), both runnable end-to-end.
- [x] **Self-sufficient, agnostic library per RFP-001** — `lez-approval/` ships `Authority`, `ApprovalError`, and the `gate` / `rotate` / `revoke` primitives. The crate depends only on `nssa_core::account::AccountId` and is reusable by any LEZ program that wants single-admin gating.

### Usability

- [x] **Module/SDK for Logos modules** — `wallet/src/program_facades/token.rs` exposes typed builders for the three new instructions; `wallet/src/cli/programs/token.rs` wires them as CLI subcommands. `demo.sh` exercises both surfaces end-to-end.
- [x] **IDL for the updated Token program** — Two artifacts: `artifacts/token.idl.spel.json` (SPEL-emitted via `spel generate-idl` against the `spel-sidecar/` sidecar — provenance) and `artifacts/token.idl.json` (hand-authored against the `SpelIdl` schema — completeness, including the `errors` table for `ApprovalError::{Unauthorized, Renounced}` which the v0.4.0 CLI does not yet emit). See `docs/SPEL_STATUS.md` for the workaround rationale and reproducibility steps.

### Reliability

- [x] **Atomic rotation/revocation** — Both handlers either return updated post-states or panic. The `Authority` state machine has no intermediate state: `rotate` is a single field write to `authority: Authority::new(new_admin)`, and `revoke` is a single field write to `authority: Authority::renounced()`. Covered by integration tests `rotate_then_mint_succeeds` and `revoke_after_authority_set_succeeds` (both in `integration_tests/tests/token_authority.rs`).
- [x] **Deterministic revoked-mint rejection** — `MintWithAuthority` against a renounced definition panics with `ApprovalError::Renounced`, a documented error variant in `lez-approval/src/lib.rs`. Covered by integration test `mint_after_revoke_panics`. The panic-on-failure path is the canonical LEZ guest rejection mechanism.

### Performance

- [x] **Compute unit (CU) cost documentation** — LEZ has no Solana-style per-instruction CU metric; the RISC0-native proxy is user-cycle count from `risc0_zkvm::default_executor`. Cycle counts captured for all three new instructions via `integration_tests/src/bin/cycle_executor.rs` and documented in `docs/CYCLE_COSTS.md` (raw measurement log) and `README.md` (summary table with caveats). Numbers on the committed `artifacts/program_methods/token.bin`: `MintWithAuthority` 154 858 cycles, `RotateAuthority` 127 350 cycles, `RevokeAuthority` 103 913 cycles (all single-segment, 2^18 padded).

### Supportability

- [x] **Deployed and tested on standalone sequencer** — `demo.sh` runs the full lifecycle against a docker-compose-spun standalone LEZ sequencer.
- [x] **End-to-end integration tests in CI** — `integration_tests/tests/token_authority.rs` (five test cases) runs under `cargo nextest` against the standalone sequencer harness. Validated locally with `cargo nextest run -p integration_tests --test token_authority -j 1` (5/5 PASS).
- [ ] **CI green on default branch** — Pending CI run on the PR; will resolve when GitHub Actions checks land on this Draft.
- [x] **README end-to-end usage** — `README.md` "LP-0013 — Token program: rotatable mint authority" section now covers: standalone deployment (`demo.sh` + manual standalone-sequencer steps), copy-pasteable CLI walkthrough for `new-fungible-with-authority` / `mint-with-authority` / `rotate-authority` / `revoke-authority`, cycle cost table (linked to `docs/CYCLE_COSTS.md`), `ApprovalError::{Unauthorized, Renounced}` panic-payload table, two-IDL pointer (linked to `docs/SPEL_STATUS.md`), and explicit limitations / follow-ups.
- [x] **Reproducible demo with `RISC0_DEV_MODE=0`** — `demo.sh` runs the full mint → transfer → rotate → revoke → mint-fails sequence with `RISC0_DEV_MODE=0` and a clean docker-compose state. End-state holder balance is 1500 after the rotated-admin mints 500 onto an initial 1000 mint.
- [ ] **Narrated video walkthrough** — Pending Day 6 (final-mile work before flipping Draft → Ready).

## FURPS Self-Assessment

### Functionality

Three new instruction variants (`MintWithAuthority`, `RotateAuthority`, `RevokeAuthority`) plus the authority-aware definition constructor `NewFungibleDefinitionWithAuthority`. Pre-existing `Mint`, `Transfer`, `NewFungibleDefinition`, `InitializeAccount`, `Burn`, `PrintNft`, and `NewDefinitionWithMetadata` are unchanged. The authority model supports the three Solana-style patterns: fixed supply (mint then revoke), variable supply (active authority), and authority handoff (rotate). The renounced state is terminal — once revoked, no instruction can re-introduce an authority on that definition.

Limitation: the authority lives only on `TokenDefinition::Fungible` (not on the non-fungible variant). The non-fungible path was out of scope for this prize.

### Usability

Wallet integration exposes typed `program_facades/token.rs` builders and CLI subcommands for `mint-with-authority`, `rotate-authority`, and `revoke-authority`. The CLI mirrors the existing Token-program subcommand idiom; no new flags or env vars beyond the existing wallet setup are required.

The IDL story is the most reviewer-visible Usability tradeoff. Quoting `docs/SPEL_STATUS.md`:

> This solution ships **two** IDL files at `artifacts/`:
>
> - `artifacts/token.idl.spel.json` — `spel generate-idl` (real SPEL toolchain, v0.4.0) — **Provenance** — proves the program shape parses through SPEL's macro grammar
> - `artifacts/token.idl.json` — hand-authored against the `SpelIdl` schema in `spel-framework-core` — **Completeness** — includes shapes the v0.4.0 CLI does not yet emit (e.g. the `errors` table for `ApprovalError::{Unauthorized, Renounced}`)
>
> The straightforward path — annotate `program_methods/guest/src/bin/token.rs` directly with `#[spel_framework::lez_program]`, depend on `spel-framework` from the workspace, and let the macro emit the IDL during the normal guest build — does **not** compile in this checkout. The reason is a dep-graph collision between two versions of the same crate, plus a downstream cross-compile failure that the collision makes structurally difficult to fix.

Reviewers can reproduce the SPEL-emitted IDL via:

```bash
cargo install --git https://github.com/logos-co/spel --tag v0.4.0 spel
spel -- generate-idl spel-sidecar > artifacts/token.idl.spel.json
```

### Reliability

The single-admin state machine has three states (`Some(admin)`, transitioning `Some(admin) → Some(new_admin)` via rotate, and the terminal `None` via revoke) with no intermediate or unreachable states. The panic-on-failure semantics inherited from the LEZ guest convention mean every authorization failure is an unrecoverable rejection that the prover catches; there is no path to a partially-rotated or partially-revoked state. The five integration tests in `integration_tests/tests/token_authority.rs` cover: mint-with-active-authority, mint-by-wrong-signer-panics, rotate-then-new-admin-mints, revoke-then-mint-panics, and rotate-after-revoke-panics.

### Performance

CU cost measurement is pending (Day 5). Expected pattern: the three new instructions add one `is_authorized` check and one `AccountId` equality check per gated call, on top of the baseline `Mint`'s arithmetic. Wall-clock proof-generation overhead should be in the noise relative to the baseline.

### Supportability

- **Test coverage:** Unit tests in `programs/token/src/tests.rs` cover the `Authority` state machine transitions and the gate / rotate / revoke primitives in isolation (8 tests across the rotate / revoke / mint-with-authority paths). Integration tests in `integration_tests/tests/token_authority.rs` cover the end-to-end lifecycle against a real standalone sequencer (5 tests).
- **CI:** Existing `cargo nextest` harness; new test file slots into the existing integration-test workflow without additional configuration.
- **Deployment:** `demo.sh` is the canonical end-to-end deployment + exercise script. Reviewers can run it from a clean checkout with `RISC0_DEV_MODE=0 ./demo.sh`.
- **Code structure:** Authority logic is split between the agnostic `lez-approval/` crate (reusable) and the Token-program-specific handlers in `programs/token/src/{new_definition,mint,rotate}.rs`. The `lez-approval` boundary makes the same pattern reusable for any future LEZ program that wants single-admin gating.

## Supporting Materials

- **Companion repo:** <https://github.com/ego-errante/logos-execution-zone/tree/lp-0013-token-authorities>
- **SPEL IDL disclosure:** [`docs/SPEL_STATUS.md`](https://github.com/ego-errante/logos-execution-zone/blob/lp-0013-token-authorities/docs/SPEL_STATUS.md)
- **Sidecar scaffold:** [`spel-sidecar/`](https://github.com/ego-errante/logos-execution-zone/tree/lp-0013-token-authorities/spel-sidecar)
- **Demo script:** [`demo.sh`](https://github.com/ego-errante/logos-execution-zone/blob/lp-0013-token-authorities/demo.sh)
- **Narrated video:** _pending Day 6_

## Terms & Conditions

By submitting this solution, I confirm that I have read and agree to the [Terms & Conditions](../TERMS.md).
Loading