Solution: LP-0013 — Token program authorities#62
Conversation
✅ Validation passedA reviewer will assess against the prize criteria. Automated check. See solution template and TERMS. |
- 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).
|
Day-5 polish landed (HEAD
Ready for review pending the narrated video walkthrough (Day 6 — required to flip Draft → Ready per the prize spec). All other success criteria are addressed and the corresponding checkboxes in |
|
Appreciate your work towards a Lambda prize. In terms of housekeeping, we close all PRs in draft mode. Please do a PR only with a complete solution. Thank you. |
|
Noted @mart1n-xyz ! opening a new one soon |
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 — 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
lp-0013-token-authorities47040d14lez-approval/src/lib.rs— RFP-001 agnostic approval library (Authority,ApprovalError)programs/token/core/src/lib.rs—Instructionenum extensions and the canonicalAuthorityfield onTokenDefinition::Fungibleprograms/token/src/{new_definition,mint,rotate}.rs— handler implementationsprogram_methods/guest/src/bin/token.rs— guest dispatcherwallet/src/{program_facades,cli/programs}/token.rs— wallet facade + CLI subcommandsexamples/{fixed-supply,variable-supply}/— two example integrationsintegration_tests/tests/token_authority.rs— end-to-end lifecycle testsartifacts/token.idl.spel.json— SPEL-emitted IDL (provenance, generated fromspel-sidecar/)artifacts/token.idl.json— hand-authored canonical IDL (completeness, conforms toSpelIdl)spel-sidecar/— sidecar Cargo package outside the workspace that hosts the SPEL-shape mirrordocs/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 singleInstructionenum dispatched byprogram_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-existingMintandNewFungibleDefinitionpaths 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: Authorityfield onTokenDefinition::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 theauthorityfield is a breaking Borsh-layout change for pre-existingTokenDefinition::Fungibleaccounts. This is acceptable today (LEZ does not promise in-flight schema-compatibility across upgrades), but theTokenDefinitiondoc comment inprograms/token/core/src/lib.rssketchesFungibleV2and separate-PDA alternatives as deferred options for if LEZ ever does.lez-approvalas the RFP-001 deliverableThe agnostic approval library is implemented as a new top-level workspace crate,
lez-approval. It exposes:Authority(Option<AccountId>)— a single-admin wrapper whoseNonestate is terminal (the renounced sentinel). TheOptionis intentionally hidden behind constructor (Authority::new,Authority::renounced) and predicate (Authority::is_renounced) methods so callers cannot accidentally reach into theNonestate 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.Unauthorizedcovers both "the wrong signer" and "the signer is not authorized at all";Renouncedcovers all post-revocation rejections.gate/rotate/revokecontract 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
ApprovalErrorpayload rather than returning aResult. This matches the surrounding code and avoids each handler having to writematch … panic!()boilerplate. The library is generic — it depends only onnssa_core::account::AccountIdand 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.
rotate_authorityeither updates the definition account'sauthorityfield or panics; there is no intermediate state where the authority isNonebut 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
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.Instruction::Mintto 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 onprogram_methods/guest/src/bin/token.rsto get a real-program SPEL IDL. Attempted and abandoned — pullingspel-frameworkinto the workspace causes a hardnssa_corev0.1.0 vs v0.2.0-rc3 dep-graph collision (#[lez_program]proc-macro emits path-literal references to whichevernssa_corecargo 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 indocs/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
NewFungibleDefinitionWithAuthoritysets the initial authority at definition creation;MintWithAuthoritymints gated by the recorded authority;RotateAuthorityandRevokeAuthoritycover rotation and terminal revocation (renounced state is a permanent sentinel viaAuthority::renounced()).examples/fixed-supply/(mint everything, then revoke) andexamples/variable-supply/(rotatable inflation), both runnable end-to-end.lez-approval/shipsAuthority,ApprovalError, and thegate/rotate/revokeprimitives. The crate depends only onnssa_core::account::AccountIdand is reusable by any LEZ program that wants single-admin gating.Usability
wallet/src/program_facades/token.rsexposes typed builders for the four new instructions;wallet/src/cli/programs/token.rswires them as CLI subcommands.demo.shexercises both surfaces end-to-end.artifacts/token.idl.spel.json(SPEL-emitted viaspel generate-idlagainst thespel-sidecar/sidecar — provenance) andartifacts/token.idl.json(hand-authored against theSpelIdlschema — completeness, including theerrorstable forApprovalError::{Unauthorized, Renounced}which the v0.4.0 CLI does not yet emit). Seedocs/SPEL_STATUS.mdfor the workaround rationale and reproducibility steps.Reliability
Authoritystate machine has no intermediate state:rotateis a single field write toauthority: Authority::new(new_admin), andrevokeis a single field write toauthority: Authority::renounced(). Covered by integration testsrotate_then_mint_succeedsandrevoke_after_authority_set_succeeds(both inintegration_tests/tests/token_authority.rs).MintWithAuthorityagainst a renounced definition panics withApprovalError::Renounced, a documented error variant inlez-approval/src/lib.rs. Covered by integration testmint_after_revoke_panics. The panic-on-failure path is the canonical LEZ guest rejection mechanism.Performance
risc0_zkvm::default_executor. Cycle counts captured for all three new instructions viaintegration_tests/src/bin/cycle_executor.rsand documented indocs/CYCLE_COSTS.md(raw measurement log) andREADME.md(summary table with caveats). Numbers on the committedartifacts/program_methods/token.bin:MintWithAuthority154 858 cycles,RotateAuthority127 350 cycles,RevokeAuthority103 913 cycles (all single-segment, 2^18 padded).Supportability
demo.shruns the full lifecycle against a docker-compose-spun standalone LEZ sequencer.integration_tests/tests/token_authority.rs(five test cases) runs undercargo nextestagainst the standalone sequencer harness. Validated locally withcargo nextest run -p integration_tests --test token_authority -j 1(5/5 PASS).lp-0013-token-authoritiesat47040d14: run #26419209365.README.md"LP-0013 — Token program: rotatable mint authority" section now covers: standalone deployment (demo.sh+ manual standalone-sequencer steps), copy-pasteable CLI walkthrough fornew-fungible-with-authority/mint-with-authority/rotate-authority/revoke-authority, cycle cost table (linked todocs/CYCLE_COSTS.md),ApprovalError::{Unauthorized, Renounced}panic-payload table, two-IDL pointer (linked todocs/SPEL_STATUS.md), and explicit limitations / follow-ups.RISC0_DEV_MODE=0—demo.shruns the full mint → transfer → rotate → revoke → mint-fails sequence withRISC0_DEV_MODE=0and a clean docker-compose state. End-state holder balance is 1500 after the rotated-admin mints 500 onto an initial 1000 mint.FURPS Self-Assessment
Functionality
Three new instruction variants (
MintWithAuthority,RotateAuthority,RevokeAuthority) plus the authority-aware definition constructorNewFungibleDefinitionWithAuthority. Pre-existingMint,Transfer,NewFungibleDefinition,InitializeAccount,Burn,PrintNft, andNewDefinitionWithMetadataare 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.rsbuilders and CLI subcommands formint-with-authority,rotate-authority, andrevoke-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: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.jsonReliability
The single-admin state machine has three states (
Some(admin), transitioningSome(admin) → Some(new_admin)via rotate, and the terminalNonevia 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 inintegration_tests/tests/token_authority.rscover: mint-with-active-authority, mint-by-wrong-signer-panics, rotate-then-new-admin-mints, revoke-then-mint-panics, and rotate-after-revoke-panics.Performance
The three gated new instructions each add an
is_authorizedcheck and oneAccountIdequality check on top of the baselineMint's arithmetic. Measured user-cycle counts on the committedartifacts/program_methods/token.bin(single-segment, 2^18 padded):MintWithAuthority154 858,RotateAuthority127 350,RevokeAuthority103 913. Raw measurement log indocs/CYCLE_COSTS.md; summary table in the README.Supportability
programs/token/src/tests.rscover theAuthoritystate machine transitions and the gate / rotate / revoke primitives in isolation (8 tests across the rotate / revoke / mint-with-authority paths). Integration tests inintegration_tests/tests/token_authority.rscover the end-to-end lifecycle against a real standalone sequencer (5 tests).cargo nextestharness; new test file slots into the existing integration-test workflow without additional configuration.demo.shis the canonical end-to-end deployment + exercise script. Reviewers can run it from a clean checkout withRISC0_DEV_MODE=0 ./demo.sh.lez-approval/crate (reusable) and the Token-program-specific handlers inprograms/token/src/{new_definition,mint,rotate}.rs. Thelez-approvalboundary makes the same pattern reusable for any future LEZ program that wants single-admin gating.Supporting Materials
docs/SPEL_STATUS.mdspel-sidecar/demo.shTerms & Conditions
By submitting this solution, I confirm that I have read and agree to the Terms & Conditions.