Skip to content

feat(exo-0d3): lez-msig — an M-of-N multisig on LEZ, private by default, run from the room - #344

Merged
corpetty merged 28 commits into
mainfrom
feat/lez-msig
Oct 24, 2026
Merged

corpetty merged 28 commits into
mainfrom
feat/lez-msig

Conversation

@corpetty

@corpetty corpetty commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Summary

lez-msig (epic exo-0d3): an M-of-N multisig on LEZ that is private by default, with a muster room as its coordination layer. It answers RFP-005 (logos-co/rfp). Design and decision log: docs/design/lez-msig.md.

  • The program (programs/lez-msig/), new, rather than a port of lez-multisig:

    • msig_core holds every rule as pure functions with no LEZ dependency. 24 tests, two of them property tests.
    • A thin v0.3 run_program(plan, apply) adapter. The rules sit outside it, so moving to dev's transactional actors (LEZ PR #935) is another thin adapter.
    • Members approve with per-multisig BIP-340 keys. The approvals are collected in the room and verified inside the program at execution (R.1).
    • A config epoch voids older approvals (R.2; the cutoff is the queue), plus roles, expiry, a time lock with immediate cancels, spending limits, weights, an action set, and a 256-nonce window against replay.
    • The plan re-derives the multisig's own PDA and refuses any other account, which closes the exo-325 class (one quorum moving another multisig's vault) by construction.
  • Both postures, one program:

    • Public PDA: driven from the room end to end on a local LEZ v0.3 zone (module/tests/lez_msig_live_e2e.nim).
    • Group-owned private PDA: create, shield and a 2-of-3 payment, each one privacy-preserving transaction accepted by a live sequencer. Nothing public exists at the multisig's id (programs/lez-msig/host/tests/live_private.rs).
  • Real-proving benchmark (programs/lez-msig/BENCHMARKS.md, 64-core CPU, RISC Zero's accelerated k256): a private payment with M approvals proves in 150.7 s at M=1, 208.5 s at M=5 and 288.1 s at M=10. The transaction is a constant 228 KB. M ≥ 5 is viable (P.2).

  • Muster:

    • The lez.msig driver: the effect is the proposal's exact bytes, which the card decodes (U.6). Its contributor is msig:<key>, attested by that same key.
    • The creation ceremony: approval keys proved by themselves and author-signed; the opener creates the multisig and discloses it.
    • A chain seam, and a settlement that chooses Execute, Queue or ExecuteQueued from the program's state and judges finality by state, never by inclusion.
    • Hosted methods lez_msig_open, lez_msig_ceremonies, coordinate_propose_lez_msig, lez_msig_state, and Settings → lez-msig-program.
    • Nim's copy of the program's bytes (lez/msig.nim) is held byte for byte to vectors the program's own host generates.
    • Registered everywhere a family must be: the registry entry, kinds, profile, conformance, readiness, and the action corpus and atlas.
  • The UI (exo-e38): the room's accounts panel creates a multisig (public or private), shows who has joined, pays, sets the time lock and cancels a queued action. The card decodes the signed bytes: the amount in LEZ, the expiry, and the approvers by name. Seen on a display (Xvfb, two runners); scripts/lez-msig-self-test.sh drives two real runners through create → propose → approve → settle.

  • The private posture from the room (exo-efa):

    • The prover. lez_msig_module, a Rust Logos module (programs/lez-msig/module, .#lgx on logos-module-builder 0.3.3), proves and sends private transactions. It holds no key: a deposit is proved from the payer's public key, signed in the member's keystore, and sent by send.
    • The room. The opener seals a fresh group secret to each member. After each included transaction, the member who sent it seals the new note to everyone, so members read the multisig without scanning the chain.
    • Settlement. MsigPrivate reads the state from the newest note and sends through the prover, so the settlement rules are the public posture's code.
    • What exists. A lez.msig-private family, a lez_msig_fund method, and the UI's "Create a private…" and "Put in from my account" buttons.
  • infra/lez/localnet.sh prefers the host's libclang and libpcsclite, because nixpkgs' copies fail on Ubuntu 24.04's glibc.

  • Recovery (exo-abc): a member whose room never got the newest note reads it back from the chain with the group's viewing key (lez_msig_recover, also started automatically when a private settle fails).

  • Release 0.3.3:

    • lez_msig_module is published in Muster's catalogue and bundled in the runner and the AppImage. The published muster_ui does not require it.
    • Delivery is accepted at >=0.3.2. Basecamp's default, delivery 0.4.0 (the same code as 0.3.3), had made it refuse every fresh catalogue install of Muster.
    • muster-v0.3.3 is published from 6a74158. Its catalogue index is catalog: Muster 0.3.3, and lez_msig_module 0.1.0 #345; merge this PR first.

Not in this PR:

  • spending limits proposed from the room;
  • updating room membership on a config change;
  • testnet runs.

Tests

  • module/tests/run-suite.sh: the whole suite (re-run on the final head before merge).

  • module/tests/run-suite.sh e2e lez_msig_live on infra/lez/localnet.sh, with the program deployed (MUSTER_LEZ_MSIG_BIN).

  • module/tests/run-suite.sh e2e lez_msig_private (dev-mode zone, the msig_exec CLI as prover), 7/7:

    • the group secret: one per multisig, sealed per member;
    • Create through the prover, and its note read by every member;
    • a deposit signed in the keystore;
    • a 2-of-3 payment;
    • the chain's copy names no member key or create key;
    • an overdraft is refused while proving;
    • below threshold, nothing is sent.
  • On a display (two runners, Xvfb, a dev-mode local zone), the private path ran through the bundled prover:

    • create a private 2-of-2: disclosed in 66 s;
    • deposit 5000 from A's own account;
    • B approves;
    • Settle on-chain clicked on A's screen: final, vault 3800.

    The settled line now shows the transaction hash, not the prover's job id.

  • cargo test -p lez_msig_module; scripts/lez-msig-self-test.sh (two runners); scripts/ui-lint.sh 0 new.

  • In programs/lez-msig: cargo test (core and the in-process state machine), the live private test against the local zone, and msig_bench with real proving.

🤖 Generated with Claude Code

…ult (design, core, v0.3 program)

RFP-005's multisig, with the rules in one place and the room as its coordination layer
(docs/design/lez-msig.md):

- msig_core: the rules as two pure functions with no LEZ dependency. claims() is what the
  instruction alone says (the window, any outflow), for a plan that sees no data; apply() is
  every check against the stored state. Approvals are BIP-340 by per-multisig keys, verified
  in-program (R.1): distinct members, APPROVE role, weighted, proposer signed. A config epoch
  every approval commits to (R.2, cutoff = the queue), time lock via a pessimistic ETA,
  expiry, cancels that are never time-locked, roles, an action set and target list, spending
  limits per period, and a 256-nonce window against replay.
- msig_v03 + the guest: the v0.3 run_program(plan, apply) adapter. The plan re-derives the
  multisig's account from the create key's seed and its placement (public PDA, or a
  group-owned private PDA) and refuses any other row, so a counterfeit state is never granted
  the seed (the exo-325 class, closed by construction). Outflows are chained calls granted
  the multisig's own seed only.
- infra/lez/localnet.sh: prefer the host's libclang and libpcsclite; nixpkgs' need a glibc as
  new as their own and fail on Ubuntu 24.04's 2.39.

Tested-Behavior: programs/lez-msig/core/tests/rules.rs (24 tests, cargo test): R.1 approvals, R.2 epoch + queue cutoff, time lock/expiry/cancel, roles, action set, spending limits, nonce window (properties); the guest builds (cargo build -p msig_methods)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
…ted guest, muster's Nim port of its bytes

- msig_host: one multisig at its placement. A public PDA, or a group-owned private PDA whose
  keys every member derives from the group master secret (key_protocol's GroupKeyHolder).
  It builds rows and envelopes from the core instruction alone, so host and guest agree by
  construction; builds public or privacy-preserving transactions; and decrypts the
  multisig's new note as a member would.
- host/tests/v03_state.rs, on LEE's own V03State: the public posture end to end (create with
  funds already sent, 2-of-3 pay, below threshold moves nothing); a state anywhere but its
  PDA refused; rows other than the outflow's refused; a time lock landing only from its ETA;
  R.2 on chain; a spending limit; and the private posture: create a private PDA, shield funds
  into it, pay 2-of-3 out of it with both approvals verified inside the proof. The chain
  never holds a public account for the multisig.
- The guest uses RISC Zero's accelerated k256 / crypto-bigint / sha2 forks (guest workspace
  only).
- module/src/lez/msig.nim: muster's port of the types, borsh, tagged hashes and public PDA,
  held byte for byte to vectors the program's host generates. A BIP-340 signature made in
  Nim equals the host's.
- msig_bench: RFP P.1/P.2 with real proving (refuses RISC0_DEV_MODE).

Tested-Behavior: cargo test -p msig_host (8, public + private posture on V03State); module/tests/run-suite.sh unit lez_msig_bytes (every encoding, hash, envelope and signature against the program's vectors)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
…2e against the real program

A muster room creates and uses a lez-msig multisig end to end on a LEZ v0.3 zone:

- drivers/lez_msig.nim (family lez.msig): the account is the multisig's public PDA,
  re-derived from (program, create key) on disclosure. The effect is the proposal's exact
  borsh bytes, so the card's decode is of what approvals sign (RFP U.6). canonicalize
  commits to the proposal id under the chain. A contribution is a BIP-340 approval by a
  member's per-multisig key, named msig:<key>, signed and attested in-app by that one key.
- coordination/msig_room.nim: the creation ceremony (lmsig/<ck>/open, author-signed
  lmsig/<ck>/key/<who>, each approval key proved by itself over room + create key +
  identity), and proposing at the chain's epoch and the next nonce free on chain and in the
  room. Proposing is the proposer's approval.
- wallet/lez_msig_chain.nim: the chain seam, live over the user's own sequencer. Every
  instruction goes from the member's own funded account, which pays its fee.
- settlement LezMsigSettlement: reads the program's state and sends ExecuteQueued,
  Queue or Execute with the driver-verified approvals. Refuses a stale epoch, a spent nonce,
  an expired proposal or too little weight, by name. watch judges by state (a refused call
  is included on v0.3), and executes a queued action itself once its lock ends.
  Settlement.watchDeadlineS lets a time-locked watch outlive the default.
- attest: msig:<key> attestations verify as BIP-340 over keccak256(P).

Tested-Behavior: lez_msig_live_e2e on infra/lez/localnet.sh with the real program (ceremony, create from a member's account, disclose, 2-of-3 pay in one tx, R.2 stale-epoch refusal, time lock queued then executed by the watch, below threshold sends nothing); lez_msig_driver_test (disclosure, effect, contributions, in-app sign + attest, weights, conformance)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
…gistry + corpus, real-proving benchmark

- muster.lidl: lez_msig_open, lez_msig_ceremonies, coordinate_propose_lez_msig,
  lez_msig_state (regenerated muster_gen.nim). Settings → lez-msig-program.
- muster_module: the intents tick joins every ceremony that invites this member, and the
  opener's client sends Create from its own account and discloses the account once the
  chain holds it. coordinate_submit reaches LezMsigSettlement over the member's own
  sequencer. Approvals read as this member's own (myNames: msig:<key> per disclosed
  multisig). A disclosure takes members, k and config from the program's state, never from
  the discloser. A pending settle may wait out its time lock (watchDeadlineS).
- readiness grades lez-msig-key from the driver's own answer.
- The family registry gains lez.msig (built, contract locus, explicit binding); the action
  corpus gains lez-msig/transfer and lez-msig/config, with an authored lez.msig.json
  (create, transfer, config); the atlas and the landscape table are regenerated.
- programs/lez-msig/BENCHMARKS.md, real proving on a 64-core CPU. A private payment with M
  approvals: 150.7 s (M=1), 208.5 s (M=5), 288.1 s (M=10), about 15 s per approval. The
  transaction is a constant 228 KB. Accelerated k256 is 2.9x stock. M >= 5 is viable (P.2).

Tested-Behavior: module/tests/run-suite.sh (277/277: kinds, profile, conformance, phase A exit, action corpus with lez-msig; the hosted surface compiles in probe_return_marshalling_host); msig_bench with real proving
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
…cutor (msig_exec)

- host/tests/live_private.rs, against a real LEZ v0.3.0 sequencer with the loader-deployed
  program. A group-owned private PDA is created (only a commitment on chain, nothing public
  at the multisig's id), funded by a shield, and pays a public recipient 2-of-3 in one
  privacy-preserving transaction; below threshold, it cannot even be proved.
- bin/msig_exec: the private posture's executor for muster (derive / exec / fund), JSON in
  and out. It proves against the chain's membership proof for the multisig's current note
  and answers with the new note in plaintext, for the room to carry sealed.

Tested-Behavior: MSIG_LIVE_RPC=http://127.0.0.1:3040 MSIG_LIVE_PROGRAM=<deployed> RISC0_DEV_MODE=1 cargo test -p msig_host --test live_private (create, shield, 2-of-3 pay on the live local zone; below threshold refused at proving)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
TDD-Exempt: issue tracking only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
…card decodes the signed bytes

- The room's accounts panel gains "LEZ multisig (approvals in the room)":
  - open a ceremony (M of the room's members, a time lock in minutes), and see each
    ceremony's progress until it is created and disclosed;
  - the account as its program holds it (vault, M of N, epoch, time lock, queue);
  - propose a payment, a new time lock, or the cancel of a queued action.
  Proposing sets the room's policy to lez-msig@<account> first.
- The card shows a lez-msig proposal decoded by the module from the exact bytes its
  approvals sign (RFP U.6): what it does, its nonce, epoch, expiry and proposal id, and that
  a configuration change or the expiry voids it. effectSummary reads the same bytes, so the
  card's line and the room history agree.
- muster_ui.rep: lezMsigOpen, lezMsigPropose, loadLezMsig, and the lezMsig*Json properties,
  in the C++ backend and the Nim app's (refreshed on the intents tick). lez_msig_state
  caches its chain read for 4 s.

Tested-Behavior: lez_msig_driver_test (the card's summary line from the decoded bytes; non-decoding bytes named unknown); probe_return_marshalling_host compiles the hosted surface; the QML is linted and seen on display in the runner next
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
…d the self-test

- The C++ backend's autopilot drives the slots the accounts panel and the card call:
  MUSTER_AUTOMSIG_OPEN, _PAY, _APPROVE, _SETTLE. Each tick logs MSIG (the payer
  account, the ceremonies, the state, the intents).
- scripts/lez-msig-self-test.sh: two offscreen runners in one room, the program deployed on
  infra/lez/localnet.sh. A opens a 2-of-2; both join with their own approval keys; A creates
  it and discloses it; the script funds the vault; A proposes a payment, B approves, A
  settles. It passes when the recipient holds exactly the payment and both runners fold the
  intent final.
- msigTick sends Create again if a minute passes with nothing on chain (an unfunded
  account's Create, or one the sequencer dropped, lands nothing).

Tested-Behavior: scripts/lez-msig-self-test.sh, run against the runner build next
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
…, for the CLI and the Logos module

Tested-Behavior: msig_exec derive / missing field / bad JSON answer as before; cargo build -p msig_host

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
…rences, plain words (no nonce / epoch)

Tested-Behavior: scripts/ui-lint.sh: 0 new findings against the baseline

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
… (pipefail + grep -q), report real seconds

Tested-Behavior: MUSTER_FLEET=local scripts/lez-msig-self-test.sh: SUCCESS — a 2-of-2 lez-msig made in the room paid exactly 1200, both runners final

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
… Logos module

- lez_msig_module.lidl: health, derive, exec, fund (development only, behind
  LEZ_MSIG_DEV_FUND=1), job, forget; event job_done. generated/provider_gen.rs comes from the
  logos-lidl-gen the module builder 0.3.3 runs (logos-rust-sdk 040e94e8; tools/regen.sh checks
  the pin).
- module/: the trait over msig_host::exec::exec_request. A proof is a job: one at a time, on
  its own thread under catch_unwind, readable only by the caller that submitted it, and
  never on disk.
- The guest is a committed artifact (artifacts/msig.bin, the deployed build). methods/build.rs
  embeds it and computes its image id, so a build needs no risc0 guest toolchain and the
  program's identity cannot drift.
- lee with "prove": risc0 proves in process (LocalProver), as LEZ's own wallet-ffi does, not
  through an r0vm on PATH.
- exec answers carry the commitment-set root the membership proof was read against, so muster
  can record it as an external read (invariant 10).

Packaging (flake.nix, metadata.json, the lgx) is the next step.

Tested-Behavior: MSIG_LIVE_RPC=… RISC0_DEV_MODE=1 cargo test -p lez_msig_module (health, derive, refusals; a private Create on the live local zone as a job: answered at once, busy meanwhile, done with the derived account's note, gone after forget); cargo build --release with lee/prove
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
…provers by name, no nonce/epoch

Seen on a display (Xvfb, two runners): the card, the accounts panel, Settle on-chain and the
panel's propose form all work. Fixed what looking showed:
- amounts went through a float ("1.2e-06 LEZ", "Vault 5e-06 LEZ"): base units are now turned
  into an exact decimal string, and the summary's amount and unit are LEZ;
- the expiry was a long date: now the locale's short form;
- the other approver read "msig:b17…": a lez-msig approval key is named as the member whose
  ceremony join bound it (memberName → msigMemberOf);
- the summary line on Home and the banner said "nonce 0, epoch 0": now "proposal #0".

Tested-Behavior: lez_msig_driver_test (the summary in LEZ, "proposal #0", no "nonce"); scripts/ui-lint.sh 0 new; probe_return_marshalling_host compiles; looked at again on a display after the rebuild
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
…is signed in the keystore

- packaging: metadata.json, CMakeLists.txt and flake.nix build `.#lgx` on logos-module-builder
  0.3.3 (Basecamp 0.3.2's generation). LEZ v0.3.0's artifacts/ reach the vendored lee and
  programs crates through a setup hook; the circuits, rapidsnark and the recursion zkr are
  pinned as LEZ's own flake pins them. linux-amd64 only for now.
- fund no longer takes a secret. It proves the deposit from the payer's public key and
  answers with the message hash to sign; the new `send` method takes the signature back,
  checks it against the message, and sends. No key reaches the module, so fund is no longer
  development-only.
- msig_vectors gives a note vector: muster decodes the multisig's account from a note.

Tested-Behavior: cargo test -p lez_msig_module (fund without a request → bad-request; send with an unreadable message → failed); nix build .#lgx built the module before this change (18 min), and a rebuild with it is running; lez_msig_private_e2e (muster) signs and sends a deposit through this path on a local zone
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
…ez_msig_module

The private posture: a group-owned private PDA whose state is a note only members read. Each
action is one privacy-preserving transaction, with no member, approval, config or create key
on chain.

- the room: lez_msig_open {private: true} seals a fresh group secret to each invited member
  (lmsig/<ck>/gms/<who>; only the opener's copy counts). After every transaction the chain
  includes, the member who sent it seals the new note to every member
  (lmsig/<ck>/note/<n>/<who>). Both are author-signed (authorship.nim).
- the prover seam (wallet/msig_prover.nim): LpMsigProver reaches lez_msig_module over lp_*
  (msig_prover_lp.nim, kept apart so tests link without lp_*); CliMsigProver runs msig_exec.
- the chain seam (wallet/lez_msig_private.nim): MsigPrivate is MsigLive except that it
  - reads the state from the newest note,
  - sends the instruction through the prover,
  - follows the job, then the transaction.
  So LezMsigSettlement's rules are one piece of code for both postures. A refusal surfaces
  while proving, and nothing is sent.
- a deposit: lez_msig_fund proves a transfer from the member's own public account, signs its
  hash in the keystore, and sends it.
- the host: the opener's tick creates through the prover and discloses the account
  (placement private). A member holding the group secret checks the address through the
  prover; the rest of the room reads unknown.
- family lez.msig-private (registry, profile: the policy and signers are never revealed),
  with its actions in the atlas corpus.
- the UI: "Create a private M-of-N LEZ multisig", the private note on the vault, and "Put in
  from my account".

Tested-Behavior: lez_msig_private_e2e on a local LEZ v0.3.0 zone in dev mode, 7/7:
  - every member opens the one sealed secret; an outsider opens none;
  - Create goes through the prover and all three read its note;
  - a deposit is signed in the keystore;
  - Bob proposes, Carol approves and settles: the recipient is paid and everyone reads 3800;
  - the chain's copy names no member key and no create key;
  - an overdraft is refused while proving;
  - below threshold, nothing reaches the prover.
Also lez_msig_driver_test (private placement, family, rvNever), profile_test (the private
family declared), the whole suite 276/277 before the profile fix and green after it,
probe_return_marshalling_host, ui-lint 0 new, check-family-registry, check-action-corpus.
Not seen on a display yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
…r + catalogue), exo-abc (note recovery)

TDD-Exempt: issue tracker events only
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
…re's prover

muster_ui depends on lez_msig_module (programs/lez-msig, this tree's copy, like
muster_module), so the standalone runner loads it and the UI's "Create a private…" reaches a
prover. muster_module declares it as an optional dependency, so Basecamp under
--access-policy enforce lets muster call it, and a host without it still loads muster.
make build overrides and relocks it alongside muster_module.

TDD-Exempt: build wiring; verified by make build and a display run of the private posture
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
…posits into it

MUSTER_AUTOMSIG_PRIVATE opens the ceremony private (the panel's "Create a private…");
MUSTER_AUTOMSIG_FUND=<amount> deposits from the member's own account once the private
account is disclosed (the panel's "Put in from my account").

TDD-Exempt: self-test hooks; exercised by the private display run after make build
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
The builder puts only `dependencies` in the runner's module set, so bad95fc made the prover a
hard dependency of muster_ui. That would make every Basecamp install of Muster require a
module that builds for linux-amd64 alone. ui/flake.nix now builds the runner from a copy of
metadata.json with lez_msig_module added; the published .lgx keeps the real one. muster_module
still declares it optional, so Basecamp loads it wherever it is installed.

TDD-Exempt: build wiring; verified by make build (the runner loads lez_msig_module) and the private display run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
… release builds lock the prover input

The runner reads the icon relative to its metadata file, so a builtins.toFile config (in
/nix/store itself) failed: "cannot stat /nix/store/src/icons/icon.png". The runner's config is
now ui/metadata.runner.json, and evaluation refuses it unless it is metadata.json with
lez_msig_module appended. scripts/catalog-release.sh and the lgx-variants workflow override
the UI flake's new relative lez_msig_module input as they do muster_module.

TDD-Exempt: build wiring; nix eval .#runner passes, make build follows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
…saction hash, not the prover's job

Seen on a display: two runners made a private 2-of-2 through the bundled lez_msig_module
(created and disclosed in 66 s), A deposited 5000 from its own account, B approved, and Settle
on-chain on A's screen went final with the vault at 3800. The settled line and Copy hash showed
"job:403b…", the prover's job id. LezMsigSettlement.chainRefOf gives the transaction's hash once
the prover has sent it ("" before, so nothing is recorded early), and the pump records that.

Tested-Behavior: lez_msig_private_e2e 7/7 (on final, chainRefOf is the 64-hex transaction hash the prover sent); probe_return_marshalling_host compiles the host
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
…hain

The member whose transaction lands seals the new note into the room. If they go away first,
the others hold a spent note, and no proof can be made over it.

- lez_msig_module gains scan: it walks every block from `from` to the tip (getBlockRange) and
  decrypts each privacy-preserving transaction's private actions with the group's viewing key
  (Msig::decrypt_actions). Each note spends the one before, so the newest is the state. It
  sends nothing.
- MsigPrivate.recover follows the scan like any job; the note it finds becomes the state and is
  sealed to every member.
- the host: lez_msig_recover, which also starts by itself when a private settlement fails.
  The private vault gets "Read it from the chain".
- found on the way: a payment's credit to a public payee is a deferred public effect and can
  land a block after the multisig's note. The e2e now waits for it; muster already judges a
  payment by the multisig's note.

Tested-Behavior: lez_msig_private_e2e 9/9 on a local zone. §8: Carol settles but never seals her note; Alice reads 3800 from the room, recovers 3500 from the chain, and Bob reads it in the room. §9: a further payment settles from the recovered note. cargo test -p lez_msig_module; probe_return_marshalling_host; ui-lint 0 new; check-action-corpus
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
The private LEZ multisig's prover is an optional dependency of muster_module that no Logos
catalogue publishes, so Muster's does. catalog-release.sh builds it here
(programs/lez-msig#lgx-portable, linux-amd64 only, at its own version), checks its manifest,
publishes it as lez_msig_module-<version>-linux-amd64.lgx, and lets the index name it. A
--local test on another platform goes without it; a release needs a linux-amd64 host.
release-appimage.sh installs it into the AppImage, so a private multisig works there with
nothing to install.

TDD-Exempt: release scripts; exercised by the 0.3.3 catalogue release (manifest check, index validate) and the AppImage looked at
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
TDD-Exempt: a version bump; the release gate (run-suite, ui-parity, the AppImage looked at) runs on this commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
…-default multisig with an encrypted room

What is built, measured and verified, with the milestones that remain. The team, contact,
budget per milestone and the audit quote are left as [placeholders] for the operator.

TDD-Exempt: a document
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
TDD-Exempt: issue tracker events only
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
… as 0.4.0; the runner on v0.3.3

Found by the release gate, a fresh catalogue install into Basecamp 0.3.2. Basecamp's
default choice for delivery_module is 0.4.0, the newest in Logos Official since 2026-10-14,
and it then refuses to load Muster: "Delivery Module — requires ~0.3.2, found 0.4.0". Every
fresh catalogue install of Muster 0.3.2 has hit this since that day.

delivery_module 0.4.0 is logos-delivery-module 1e1fb56: v0.3.2 plus two commits (mix
routing with a shared RLN backend, #148; the LEZ v0.3.1 RLN registry for logos.test, #160).
v0.3.3 is that commit plus only its release commit, and the two contracts are identical
except for the version line. So muster_module accepts >=0.3.2, as Logos Official's
chat_module and chat_ui accept >=0.3.0. module/ and ui/ pin v0.3.3, so the runner and its
tests run the code a catalogue install gets. The AppImage keeps its tested bundle
(delivery 0.3.2), which the range still admits.

Tested-Behavior: make build on v0.3.3, then module/tests/run-suite.sh and scripts/ui-parity.sh (two-instance, invite, relaunch, splits) on that runner; a fresh catalogue install into Basecamp 0.3.2 that takes delivery 0.4.0 loads Muster
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
…sed, exo-bfd's gates recorded

TDD-Exempt: docs and tracker events
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Session-Id: 82cf96d0-9403-5a71-95d3-2ef04ee3e51c
@corpetty
corpetty merged commit 7505832 into main Oct 24, 2026
7 checks passed
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.

1 participant