diff --git a/solutions/LP-0013.md b/solutions/LP-0013.md new file mode 100644 index 0000000..7fe9b9f --- /dev/null +++ b/solutions/LP-0013.md @@ -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:** +- **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)` — 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:** +- **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).