Skip to content

Solution: LP-0013 — Token program authorities#70

Closed
ego-errante wants to merge 5 commits into
logos-co:masterfrom
ego-errante:lp-0013-solution
Closed

Solution: LP-0013 — Token program authorities#70
ego-errante wants to merge 5 commits into
logos-co:masterfrom
ego-errante:lp-0013-solution

Conversation

@ego-errante
Copy link
Copy Markdown

Solution: LP-0013 — Token program authorities

Submitted by: ego-errante

Re-submitted from #62 — that PR was closed per housekeeping policy (no draft PRs). This one is opened complete and ready for review: narrated video uploaded, all success-criteria boxes ticked, CI green at HEAD.

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 — and exposes four 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: 6b14e1c2
  • Key files:
    • lez-approval/src/lib.rs — RFP-001 agnostic approval library (Authority, ApprovalError)
    • programs/token/core/src/lib.rsInstruction 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 (6 cases)
    • 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. I layer the authority model additively: four 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. I 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 me 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 acceptable today (LEZ does not promise in-flight schema-compatibility across upgrades), but the TokenDefinition doc comment in programs/token/core/src/lib.rs sketches FungibleV2 and separate-PDA alternatives as deferred options for if LEZ ever does.

The authority account itself is claimed by the Token program on first use via AccountPostState::new_claimed_if_default(authority_post, Claim::Authorized) in every gated handler. Without this, the sequencer's nonce bump on the signer account after one MintWithAuthority would trip the validator's owner-rule on every subsequent RotateAuthority / RevokeAuthority. The trade-off is that the authority account is permanently owned by the Token program after first use, so it cannot subsequently sign for other programs — the doc comments on the four new instructions call this out explicitly with the guidance: use a dedicated keypair per mint authority, do not reuse an existing wallet account.

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 I 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 me this for free, which is why I 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.
  • Read-only signer post-state (no claim) for the authority account. Rejected — the validator does a strict 1:1 positional zip of pre- and post-states (nssa/core/src/program.rs) and the sequencer's nonce bump on a signer is not separable from program-declared state. The Token program must emit a claimed post-state for the authority on first use, or every subsequent RotateAuthority / RevokeAuthority trips the validator's owner-rule. Investigated Claim::Pda and a hypothetical transient Claim variant; the enum has only the two existing variants and no tx-boundary reset semantics. The new_claimed_if_default call is idempotent across the mint → rotate → revoke chain, so the fix is safe at every call site.
  • 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. I 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).

Success Criteria Checklist

Functionality

  • Variable-size Tokens through minting authorityNewFungibleDefinitionWithAuthority 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()).
  • Two example integrationsexamples/fixed-supply/ (mint everything, then revoke) and examples/variable-supply/ (rotatable inflation), both runnable end-to-end.
  • Self-sufficient, agnostic library per RFP-001lez-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

  • Module/SDK for Logos moduleswallet/src/program_facades/token.rs exposes typed builders for the four new instructions; wallet/src/cli/programs/token.rs wires them as CLI subcommands. demo.sh exercises both surfaces end-to-end.
  • 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

  • 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_authority_then_mint_with_new_admin and revoke_after_rotate_uses_current_authority (both in integration_tests/tests/token_authority.rs).
  • Deterministic revoked-mint rejectionMintWithAuthority against a renounced definition panics with ApprovalError::Renounced, a documented error variant in lez-approval/src/lib.rs. Covered by integration test revoke_authority_blocks_all_subsequent_mints. The panic-on-failure path is the canonical LEZ guest rejection mechanism.

Performance

  • 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

  • Deployed and tested on standalone sequencerdemo.sh runs the full lifecycle against a standalone LEZ sequencer (built in-process via --features sequencer_service/standalone, no docker-compose required).
  • End-to-end integration tests in CIintegration_tests/tests/token_authority.rs (six 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 (6/6 PASS).
  • CI green on default branch — All 9 GitHub Actions jobs green on lp-0013-token-authorities at 6b14e1c2: run #26537927764.
  • README end-to-end usageREADME.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.
  • Reproducible demo with RISC0_DEV_MODE=0demo.sh runs the full mint → transfer → rotate → revoke → mint-fails sequence with RISC0_DEV_MODE=0 from a clean checkout. End-state holder balance is 1500 after the rotated-admin mints 500 onto an initial 1000 mint; the post-revoke mint attempt is rejected by the sequencer with Guest panicked: ApprovalError::Renounced.
  • Narrated video walkthroughhttps://youtu.be/0LcMW16Ke-M (unlisted YouTube). Covers cold-open context, architecture diagram, four key implementation decisions, and a full DEV_MODE=0 end-to-end demo against a standalone sequencer.

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.jsonspel 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-coreCompleteness — 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:

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 six integration tests in integration_tests/tests/token_authority.rs cover: mint-with-active-authority, rotate-then-new-admin-mints, old-admin-blocked-after-rotate, revoke-blocks-all-subsequent-mints, revoke-uses-current-authority-after-rotate, and the full mint-by-authority-then-rotate-then-mint-by-new-authority lifecycle.

Performance

The three gated new instructions each add an is_authorized check and one AccountId equality check on top of the baseline Mint's arithmetic. Measured user-cycle counts on the committed artifacts/program_methods/token.bin (single-segment, 2^18 padded): MintWithAuthority 154 858, RotateAuthority 127 350, RevokeAuthority 103 913. Raw measurement log in docs/CYCLE_COSTS.md; summary table in the README.

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. Integration tests in integration_tests/tests/token_authority.rs cover the end-to-end lifecycle against a real standalone sequencer (6 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

Terms & Conditions

By submitting this solution, I confirm that I have read and agree to the Terms & Conditions.

- Bump linked commit: c9be6e86 → 9e222602 (Day-5 polish: cycle bench,
  README expansion, fmt/clippy fixes).
- Tick "Compute unit (CU) cost documentation" (cycle counts captured
  via cycle_executor bin, documented in docs/CYCLE_COSTS.md + README).
- Tick "README end-to-end usage" (LP-0013 README section now covers
  deployment, CLI, cycle table, error codes, two-IDL pointer).
- Fix "three" -> "four" new instructions (3 locations); the count
  has always been four since NewFungibleDefinitionWithAuthority.
- Switch we/our/us -> I/my/me throughout (solo submission).
- Rewrite Borsh-layout trade-off paragraph: clarify it's acceptable
  today (LEZ has no schema-compat guarantee) and add file-path
  cross-reference to the TokenDefinition doc comment.
- Refresh FURPS Performance from "pending" to measured cycle counts
  (154 858 / 127 350 / 103 913).
- Bump SHA reference 9e222602 -> 47040d14 (Ubuntu artifact rebuild
  + deny / machete CI fixes).
- Tick "CI green on default branch" with run URL — 9/9 green.
- Bump commit SHA to 6b14e1c2 (Day-7 claim-on-first-use fix landed)
- Add Decision 2 narrative (claim semantic + dedicated-keypair guidance)
- Add Alternatives bullet covering the no-claim signer post-state path
- Update integration-test count 5 → 6 (regression test added)
- Refresh CI run link to 26537927764 (green on 6b14e1c2)
- Replace docker-compose phrasing with standalone --features detail
- Tick narrated-video checkbox + link unlisted YouTube
@github-actions
Copy link
Copy Markdown

✅ Validation passed

A reviewer will assess against the prize criteria.
ℹ️ Solution submission for LP-0013.
ℹ️ Checking repo: https://github.com/ego-errante/logos-execution-zone


Automated check. See solution template and TERMS.

@mart1n-xyz
Copy link
Copy Markdown
Collaborator

Appreciate the resubmit, please note that there is a one submission per week per prize policy in place as stated in the repo readme:

Each builder (or team) is allowed a maximum of 3 submissions per prize, with at most one submission/review per week.

Closing based on this; please, resubmit on May 30 or later.

@mart1n-xyz mart1n-xyz closed this May 28, 2026
@ego-errante
Copy link
Copy Markdown
Author

Sorry @mart1n-xyz , will do. Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants