- PROJECT.md
- BOOTSTRAP_TASK.md
- unified-crypto-methodology.md
- AokInterface.lean
- ZkPoPCore.lean
- FoldedReconstruction.lean
- bootstrap_transport.py
Status: proposed (not yet built/checked) Assembled from: agda-zkpop-v1, mojo-zkpop-v1, nodejs-zkpop, zk-pop-solana-lean4-v1 (transport costs 1, 1, 2, 1 — see transport solve in conversation) Excluded: mog-zk-pop-v1 (content-bearing modulus; stays its own project, referenced not merged)
A single ZkPoPGroup / ZkPoPArgument interface (see ZkPoPCore.lean, sketch)
that the four transport-selected papers implement instead of each restating
its own commitment scheme. Completeness / special-soundness / perfect-HVZK
are proved once, generically, against the interface — not per instance.
- Not yet compiled or kernel-checked. This is a design sketch, written to make the interface concrete enough to build against, not a machine-checked artifact.
- Not a re-verification of the four source papers' underlying corpora — those are intentionally withheld (that's the point of zkPoP), so this entry inherits their claims about the group/modulus choice from their published prose, same as the transport plan did. If any of those papers' actual Lean development used a different concrete group than stated, this registry entry would be wrong and needs correcting from the source, not from this document.
- Not a merge of the 27 existing UUID projects — added as an independent, new entry so nothing existing is overwritten.
| Property | Status |
|---|---|
| Group axioms (Ristretto255) | Standard, citable (RFC 9496) — not reproved here |
| Completeness/soundness/HVZK | Proved generically elsewhere against Base; this interface is designed to let 4 papers cite that proof instead of restating it — proof of this specific reuse not yet done |
| Declared parameters vs. verified | Declared only (source withheld); flagged explicitly, not glossed over |
| mog exclusion | Deliberate — its modulus is the theorem's content, not swappable |
- Get
ZkPoPCore.leancompiling against Mathlib with a realRistretto255instance ofZkPoPGroup(the one load-bearing proof obligation). - For each of the 4 included papers, write the ~5-10 line
ZkPoPArgumentinstance that plugs its existing (already-proved)prove/verifyinto the shared interface, rather than reproving anything. - Confirm via the kernel that the generic completeness/soundness/HVZK
theorems specialize correctly to each instance (should be near-free if
step 1-2 are done correctly — this is what "generic over
Base" buys you). - Register
zkpop-core-v2inprojects/PROJECTS.mdandmanifest.jsonas a new, additive project once step 3 is kernel-checked, not before.
Follows the dotagents task pattern (SYSTEM.md/AGENTS.md/SKILL.md/boot.sh) — this is that task's SYSTEM.md-equivalent instruction.
Given the current PROJECTS feature table in bootstrap_transport.py, produce
an assignment of each project to a ProjectKind (and, for zkArgument
projects, a crypto rung) that is cheap enough to trust, not provably
minimal. Do not treat this as an exhaustive-search or exact-ILP problem
once the project count grows — the LP relaxation already in
bootstrap_transport.py is the intended method, and "good enough" is defined
below, not "global optimum."
Accept a solve's output without further search if all of the following hold:
- No content-bearing project is misclassified. Any project whose
modulus_is_content = True(or equivalent declared-content flag) must land oncontentArgument, full stop — this is a hard constraint, not a cost to trade off, the same waymog's sporadic modulus was non-negotiable. - Every
zkArgumentassignment clears the 128-bit floor. Check this as a constraint before accepting the LP's cost-minimizing choice — don't let a cost-0 shortcut (like the earliernodejs → rung1bug) slip through. - Total solve cost is not the cheapest theoretically possible, just not obviously wasteful — e.g., don't accept a solve where two projects with near-identical declared features get routed to different rungs for no stated reason; do accept a solve that's a few cost-units off some hypothetical optimum if all constraints above hold.
- Every
publishOnlyassignment corresponds to a project with no published claim yet, re-checked againstPROJECT.md's own "Arguments Published" field, not assumed.
If any of 1–4 fails, fix the cost function or feature table and re-solve — don't hand-patch the output.
- Do not re-verify any project's withheld source to "improve" the feature table's accuracy beyond what's declared in its published prose. That verification is structurally unavailable (zkPoP withholds source by design) and pretending otherwise defeats the point of flagging it.
- Do not force the Nova/Groth16
FoldedExecution/Deciderlayer onto any project that hasn't stated a distributed, untrusted-peer reconstruction requirement. It applies only where that requirement is real. - Do not chase commutativity, exact optimal transport cost, or a fully populated cost-function justification before re-running — those are real follow-ups (noted in the conversation), not blockers on accepting a good-enough bootstrap now.
- Update
PROJECTSinbootstrap_transport.pywith any new/changed declared features. - Run the LP solve.
- Check the four conditions above against the output.
- If all pass: check in the result as the new
projectFitsnapshot inAokInterface.lean, replacing the previous snapshot wholesale (it's generated, not hand-edited — see prior comment in that file). - If any fail: adjust cost function or feature table (not the output), re-run from step 2.
- Re-run only when a project's declared features actually change or a new project is added — not on a schedule, not "to see if it improves."
| Component | Instances found | Problem |
|---|---|---|
| Group for Schnorr/Pedersen/Okamoto/CDS | (a) custom 2048-bit MODP safe-prime group, q picked {2,3,5}-smooth for a finite Lucas certificate; (b) 2^521−1 Mersenne-modulus group (Lucas–Lehmer) |
Two different, bespoke finite-field DL groups. Both need their own primality certificate re-verified per paper. Both are ~5–10x larger and slower than an elliptic-curve group at equivalent security. |
Second generator (h in Pedersen) |
"public hash-to-group derivation," described in prose, not a fixed spec | No single, reusable, standardized derivation — every paper could in principle derive it differently. |
| Transcript / Fiat–Shamir | Hand-written H(context‖C‖t‖root)-style strings, varying paper to paper |
Classic source of soundness bugs (weak vs. strong Fiat–Shamir, missing statement-binding, missing domain separation). Nothing enforces the same binding rule everywhere. |
| Hash function | SHA-256 (FIPS 180-4, verified against test vectors in Lean) — consistent, this one's fine | — |
| Merkle tree | Domain-separated leaves/nodes, SHA-256 — consistent, fine | — |
| Recursion/self-hosting layer | Abstract "outer proof verifies inner proof" model | Protocol-agnostic — inherits whatever the base group provides. Good design, just needs one base group to inherit from. |
| Distribution (N-of-M PNG carriers) | Erasure-coded, threshold reconstruction | Carriers aren't individually bound into the same Merkle/commitment root, so partial-carrier authentication isn't as tight as it could be. |
| Non-cryptographic content | Monster-group divisor/amicable-pair arithmetic, FRACTRAN/CRT GPU overlays, Tesla-369 numerology | Legitimate as the statement being proved in zero knowledge (fun demo content) — but only one paper (the self-hosted one) explicitly ledgers this apart from the security argument. The rest don't draw the line as clearly. |
Bottom line: the security-relevant crypto is textbook-correct wherever it's spelled out, but it is duplicated ~5 different ways instead of shared, and the numerology/group-theory "flavor" content isn't consistently fenced off from the trust boundary.
Adopt one canonical group for every Schnorr / Pedersen / Okamoto / CDS instance across all projects:
- Group: Ristretto255 — a prime-order group built on Curve25519's group, with no cofactor headaches and constant-time, misuse-resistant encoding. (Curve448/Decaf448 as the "128-bit+" option if you want a second security tier instead of the 2048-bit MODP group's tier.)
- Why it replaces both existing groups: it's smaller (32-byte elements vs. 256-byte MODP elements), faster, has no custom primality certificate to maintain (the curve parameters are already standardized and audited), and gives you one security proof to write instead of two.
- Second generator
h: derive it once, centrally, via RFC 9380hash_to_curvewith a fixed domain-separation tag (e.g."aok-zkpop-v1-generator-h"). Every paper imports this constant instead of re-deriving it. - Lean formalization path: since the Sigma-protocol proofs (completeness, special soundness, HVZK) are already generic over an abstract
Basegroup typeclass, you don't re-derive those theorems. You write one new instance proof — "Ristretto255 satisfies the group axioms + hard-discrete-log/DDH assumption used byBase" — and every existing theorem (Schnorr, Pedersen, Okamoto, CDS-OR) specializes to it automatically. This is the highest-leverage single change: one proof obligation upgrades all 27 papers' crypto layer at once.
Replace the hand-rolled H(context‖C‖t‖root) strings with a single transcript construction (Merlin/STROBE-style) used everywhere:
- Bind the protocol name and version first (
"aok-zkpop/1"). - Absorb every public parameter (group, generators, statement/Merkle root, environment hash, checker identifier) before any prover message.
- Absorb each prover message (
C,t, etc.) in a fixed order. - Derive the challenge only after all of the above — this is "strong" Fiat–Shamir, which is what your knowledge-soundness proofs actually need (weak Fiat–Shamir, which only hashes the last message, is a known way these arguments silently lose extractability).
This becomes one shared Lean module + one shared library per target language, instead of a bespoke hash string per paper.
Take the "minimality ledger" / "honest trust ledger" pattern already used in the self-hosted paper and make it mandatory for every argument, with identical fields:
- Axioms used (should stay at Lean's standard three, plus
ofReduceBool/trustCompileronly where compiled evaluation is explicitly declared) - Which properties are unconditional (hiding, HVZK) vs. computational (binding/soundness — under discrete-log hardness in Ristretto255, Fiat–Shamir in the ROM)
- What the arithmetic statement is (Monster divisors, amicable pairs, whatever) — explicitly labeled as content, not security
- What's reused vs. new per paper
This turns 27 differently-worded trust sections into one comparable, auditable table.
Right now each UUID directory re-implements its own crypto layer per language port (agda, mojo, nodejs, Solana/Lean4, CompCert, etc.). Structurally:
- Factor the group + transcript + Merkle logic into one audited core (Lean4 reference implementation + generated/ported bindings), rather than 27 independent implementations.
- Per-language ports become thin bindings that call the core's test vectors to prove interoperability, not independent reimplementations of Pedersen/Schnorr from scratch.
- This cuts your actual audit surface from "~5 bespoke crypto stacks across 27 papers" to "1 crypto core + N thin, mechanically-checked bindings."
Bind the N-of-M PNG carrier shares into the same commitment root used by the argument (e.g., Merkle-leaf per carrier, root folded into the Fiat–Shamir transcript). That way a tampered or substituted carrier is detected the same way a tampered statement is, rather than being a separate, unauthenticated erasure-coding layer.
- SHA-256 for hashing/Merkle trees — already consistent and FIPS-verified, no need to touch it.
- The recursion/self-hosting abstraction — it's protocol-agnostic and correctly designed; it just inherits the upgraded base group automatically once step 2 is done.
- The choice to prove fun arithmetic facts (Monster divisors, amicable pairs) in zero knowledge — keep it, just keep it clearly outside the security-relevant crypto core per the ledger in §4.
/-
Two-tier interface for the whole aok system, not just zkpop.
Tier 0 (Publishable) is universal -- every project (zkpop, monster, lean4,
dotagents, dasl) already satisfies it via aok-publish.py / manifest.json.
Nothing new to build; this just names the existing shape.
Tier 1 (Argument) is for projects that make a checkable CLAIM about
something -- a subset of projects, not all of them. ZkPoPArgument (see
ZkPoPCore.lean) is one refinement of Argument, for claims whose evidence
must stay hidden. Plain arguments (evidence public) are the other.
dotagents does NOT get a Tier-1 instance below, on purpose: it has not
published a claim, only tooling. Giving it a fake Statement/verify pair
would be the same error as forcing mog's content modulus onto a generic
crypto rung -- inventing structure the project doesn't actually have.
-/
/-- Tier 0: universal. Mirrors manifest.json's existing entry shape exactly,
so this is a description of what already exists, not a migration. -/
structure Publishable where
uuid : String
project : String
title : String
tags : List String
sha256 : String
files : List String
author : String
published : String -- ISO timestamp, as already stored
/-- Tier 1: for projects making a checkable claim. Deliberately minimal --
`Evidence` is a type parameter so a project chooses whether its evidence
is public (plain argument) or must stay hidden (zk argument, see
ZkPoPArgument, which additionally requires `[ZkPoPGroup G]` and routes
Evidence through a commitment). -/
class Argument (Statement Evidence : Type) where
verify : Statement → Evidence → Bool
contentOnly : Bool -- true if this claim's parameters ARE its content
-- (e.g. mog's sporadic modulus) and must not be
-- swapped for a "better" generic choice
/-- Per-project fit. GENERATED, not hand-maintained -- see
bootstrap_transport.py. This block is the checked-in snapshot of that
script's last solve; regenerate it whenever a project's declared
features change or a new project is added. Do not hand-edit this list
the way an earlier draft of this file did. -/
inductive ProjectKind
| zkArgument -- evidence hidden
| contentArgument -- evidence public but IS the claim
| plainArgument -- evidence public, ordinary check
| publishOnly -- Tier 0 only, no Tier-1 claim yet
-- GENERATED by bootstrap_transport.py, total cost 5.0 -- do not hand-edit.
def projectFit : List (String × ProjectKind × Nat) := [
("agda-zkpop-v1", .zkArgument, 1),
("mojo-zkpop-v1", .zkArgument, 1),
("nodejs-zkpop", .zkArgument, 2),
("zk-pop-solana-lean4", .zkArgument, 1),
("mog-zk-pop-v1", .contentArgument, 0),
("monster", .contentArgument, 0),
("dasl", .plainArgument, 0),
("dotagents", .publishOnly, 0),
("lean4", .publishOnly, 0)
]
/-
ZkPoPCore: a standard API that every zkPoP argument (agda, mojo, nodejs,
solana-lean4, and future instances) implements, instead of each paper
restating its own commitment/protocol from scratch.
This does NOT reprove completeness/soundness/HVZK per instance. Those are
proved once, generically, against the `ZkPoPGroup` interface. An instance
only has to prove it satisfies the interface -- exactly the move the
transport plan identified as low-cost (cost 1-2 per source) for four of
the five source papers.
-/
/-- The group interface every concrete instantiation (Ristretto255, or in
principle any other group) must satisfy. This is the ONE thing that
changes when you swap crypto cores; nothing downstream re-proves. -/
class ZkPoPGroup (G : Type) where
op : G → G → G
inv : G → G
id : G
q : Nat -- group order (must be prime)
q_prime : q.Prime
g h : G -- two generators; h via fixed hash-to-group, NOT ad hoc
h_indep : True -- placeholder: "discrete log of h w.r.t. g is unknown"
hard_dl : True -- placeholder: discrete-log hardness assumption
/-- A statement being argued: a Merkle root over the withheld corpus, plus
the environment/checker identifiers every paper already publishes. -/
structure ZkPoPStatement where
merkleRoot : ByteArray
envHash : ByteArray
checkerId : ByteArray
/-- A witness: the digest/scalar the prover actually knows, never disclosed. -/
structure ZkPoPWitness (G : Type) [ZkPoPGroup G] where
m : Nat -- the committed scalar (e.g. Merkle root mod q)
r : Nat -- Pedersen blinding factor
/-- The transcript, built by ONE shared strong-Fiat-Shamir routine, not a
per-paper hash string. Binds protocol name + all public params + all
prover messages BEFORE the challenge is derived. -/
structure ZkPoPTranscript (G : Type) where
protocolTag : String := "aok-zkpop/2"
statement : ZkPoPStatement
commitment : G
announcement: G
challenge : Nat
responses : Nat × Nat
/-- L5 Session: the actual protocol exchange (commitment, challenge,
response). Swappable independently of the L4 guarantee below -- e.g.
Schnorr vs. a different Sigma-protocol shape, same guarantee. -/
class ZkPoPSession (G : Type) [ZkPoPGroup G] where
commit : ZkPoPWitness G → G
prove : ZkPoPWitness G → ZkPoPStatement → ZkPoPTranscript G
verify : ZkPoPStatement → ZkPoPTranscript G → Bool
/-- L4 Transport: the delivery GUARANTEE a session must satisfy, proved
once, generically, against any [ZkPoPSession]. Swapping the session
(L5) does not require reproving this; swapping the guarantee's strength
(e.g. adding post-quantum soundness later) does not require touching L5. -/
class ZkPoPTransportGuarantee (G : Type) [ZkPoPGroup G] (S : ZkPoPSession G) where
completeness : ∀ w s, S.verify s (S.prove w s) = true
special_soundness : ZkPoPTranscript G → ZkPoPTranscript G → ZkPoPWitness G
perfect_hvzk : True -- placeholder: simulator distribution = real distribution
-- ZkPoPArgument (previous version) = ZkPoPSession + ZkPoPTransportGuarantee
-- bundled together. Kept as a convenience alias, not the primary interface:
class ZkPoPArgument (G : Type) [ZkPoPGroup G] extends ZkPoPSession G where
extract : ZkPoPTranscript G → ZkPoPTranscript G → ZkPoPWitness G
/-- Registry entry: what each paper actually instantiates. This is the
diagonalized manifest row, expressed as data instead of prose. -/
structure ZkPoPInstance where
name : String
groupChoice : String -- "Ristretto255" | "2^521-1 Mersenne (content)" | "sporadic-order (content)"
transportCost : Nat -- from the LP solve
contentBearing: Bool -- true only when the modulus IS the theorem (e.g. mog)
def registry : List ZkPoPInstance := [
⟨"agda-zkpop-v1", "Ristretto255", 1, false⟩,
⟨"mojo-zkpop-v1", "Ristretto255", 1, false⟩,
⟨"nodejs-zkpop", "Ristretto255", 2, false⟩,
⟨"zk-pop-solana-lean4", "Ristretto255", 1, false⟩,
⟨"mog-zk-pop-v1", "sporadic-order (content)", 1, true⟩
]
/-
Folded reconstruction across untrusted peers.
This is NOT a refinement of the existing N-of-M PNG carrier scheme --
that scheme tolerates erasure (missing shares), not corruption (wrong
shares from an adversarial peer). This interface is strictly stronger and
should be treated as a new primitive, not a drop-in upgrade.
Two separable concerns, each its own typeclass on purpose (same reasoning
as splitting ZkPoPSession from ZkPoPTransportGuarantee): reconstruction
and folding are independently swappable and should not be bundled.
-/
/-- A single slice of execution contributed by one (possibly adversarial)
peer/thread, bound to a commitment so a wrong slice is detectable, not
just missing. -/
structure ExecSlice where
threadId : Nat
commitment : ByteArray -- binds this slice's content
merklePath : List ByteArray -- authenticates commitment against the global root
payload : ByteArray -- the slice itself (opened only on reconstruction)
/-- L4-analogue for reconstruction: the guarantee is "t-of-n authenticated
slices reconstruct the execution, and any slice failing its own
Merkle path is REJECTED, not silently included." This is what turns
erasure-coding into Byzantine fault tolerance -- the authentication,
not the erasure code itself. -/
class ByzantineReconstruction (n t : Nat) where
globalRoot : ByteArray
authenticate : ExecSlice → Bool -- checks merklePath against globalRoot
reconstruct : List ExecSlice → Option ByteArray
-- succeeds iff ≥ t authenticated slices present
-- guarantee to prove: ∀ slices, (authenticated slices).length ≥ t →
-- reconstruct slices = some (true execution), independent of what the
-- remaining n-t peers (up to fully adversarial) contributed.
/-- Folding: combine per-slice execution proofs into one proof whose size
does not grow with the number of threads. Concretely: Nova-style
relaxed-R1CS folding, `U' = fold U1 U2` via a Fiat-Shamir challenge `r`
and cross-term correction. This is ASSOCIATIVE under tree-shaped
reduction (any balanced binary tree of pairwise folds over the
authenticated slice set gives a sound final instance) -- it is NOT
commutative as a raw binary op, since each fold step has a "running"
and an "incoming" role with an asymmetric cross-term. Tree-shaped
associativity, not commutativity, is what lets untrusted peers'
slices fold correctly regardless of arrival order. -/
class FoldedExecution (RelaxedInstance : Type) where
fold : RelaxedInstance → RelaxedInstance → RelaxedInstance -- Nova fold step
proveSlice : ExecSlice → RelaxedInstance
-- guarantee to prove:
-- 1. tree-associativity: for any two balanced binary trees T1, T2 over
-- the same authenticated slice multiset, foldTree T1 = foldTree T2
-- up to relaxed-instance equivalence (NOT claiming `fold` itself is
-- commutative -- only that the final folded result is tree-shape-
-- independent, which is the actual property Nova needs and gives).
-- 2. soundness transports through fold: if U1, U2 are each sound
-- (satisfy their relaxed R1CS relation), fold U1 U2 is sound.
-- Same "soundness transports, is not created" statement the
-- self-hosted paper already makes about ordinary recursion.
-- 3. a single unauthenticated/corrupted slice's `proveSlice` output
-- must make every subsequent fold containing it unsound -- folding
-- must not average a bad slice's error away.
/-- The decider: wraps the single final relaxed R1CS instance (after all
folding is done) into one short, pairing-based SNARK -- Groth16 -- so
the verifier never touches relaxed-R1CS objects directly. This is the
only place a per-circuit trusted setup is needed; the fold steps
themselves need none. -/
class Decider (RelaxedInstance Groth16Proof : Type) where
decide : RelaxedInstance → Groth16Proof
verify : Groth16Proof → Bool
-- guarantee to prove: verify (decide U) = true iff U satisfies its
-- relaxed R1CS relation -- i.e. the decider is complete and sound with
-- respect to whatever the fold tree actually accumulated.
/-- The composed guarantee this whole thing is actually for: reconstruction
succeeds AND the folded proof verifies, using only threads that pass
authentication -- explicitly not "most threads agree," which is a much
weaker and different property (that would be closer to a BFT consensus
guarantee, not a proof-of-execution guarantee). -/
structure FaultTolerantExecutionClaim (n t : Nat) (RelaxedInstance Groth16Proof : Type)
[ByzantineReconstruction n t] [FoldedExecution RelaxedInstance]
[Decider RelaxedInstance Groth16Proof] where
claim : String := "execution is reconstructible and its tree-folded Nova instance, \
once decided into a Groth16 proof, verifies from any t-of-n peers, \
with corrupted/unauthenticated contributions rejected rather than \
averaged in"
"""
Minimal optimal-transport bootstrap layer for the aok interface.
This is the thing that PRODUCES projectFit / registry entries in
AokInterface.lean and ZkPoPCore.lean -- those files should be regenerated
from this, not hand-maintained, once a project's declared features change
or a new project is added.
Division of labor, stated explicitly:
- THIS script: solves the transport LP (where does each project's
declared features minimally-cost-map onto the tier/rung lattice).
- Lean: does NOT solve LP. It only checks the one load-bearing proof
obligation per rung (e.g. "Ristretto255 satisfies ZkPoPGroup") and
typechecks the per-project instance that the plan says should exist.
- Nothing here re-verifies a project's underlying claim; source is
intentionally withheld for zkArgument projects. This only optimizes
over DECLARED parameters, same caveat as before.
"""
import numpy as np
from scipy.optimize import linprog
import json
# --- Step 1: declared features per project (hand-entered from prose, flagged as such) ---
PROJECTS = {
"agda-zkpop-v1": {"kind_hint": "zk", "evidence": "hidden", "modulus_is_content": False, "declared_group_cost_to_ristretto": 1},
"mojo-zkpop-v1": {"kind_hint": "zk", "evidence": "hidden", "modulus_is_content": False, "declared_group_cost_to_ristretto": 1},
"nodejs-zkpop": {"kind_hint": "zk", "evidence": "hidden", "modulus_is_content": False, "declared_group_cost_to_ristretto": 2},
"zk-pop-solana-lean4": {"kind_hint": "zk", "evidence": "hidden", "modulus_is_content": False, "declared_group_cost_to_ristretto": 1},
"mog-zk-pop-v1": {"kind_hint": "zk", "evidence": "hidden", "modulus_is_content": True, "declared_group_cost_to_ristretto": 80},
"monster": {"kind_hint": "content", "evidence": "public", "modulus_is_content": True, "declared_group_cost_to_ristretto": 80},
"dasl": {"kind_hint": "plain", "evidence": "public", "modulus_is_content": False, "declared_group_cost_to_ristretto": None},
"dotagents": {"kind_hint": "none", "evidence": None, "modulus_is_content": False, "declared_group_cost_to_ristretto": None},
"lean4": {"kind_hint": "none", "evidence": None, "modulus_is_content": False, "declared_group_cost_to_ristretto": None},
}
# --- Step 2: tier/kind targets, each with a hard constraint on what may land there ---
KINDS = ["zkArgument", "contentArgument", "plainArgument", "publishOnly"]
def cost_to_kind(name, feat, kind):
if kind == "publishOnly":
# Only legitimate at zero cost if the project has NOTHING published yet.
return 0 if feat["evidence"] is None else 40
if kind == "plainArgument":
return 0 if (feat["evidence"] == "public" and not feat["modulus_is_content"]) else 60
if kind == "contentArgument":
return 0 if feat["modulus_is_content"] else 60
if kind == "zkArgument":
if feat["evidence"] != "hidden":
return 60
return feat["declared_group_cost_to_ristretto"] or 60
return 999
names = list(PROJECTS.keys())
n, m = len(names), len(KINDS)
cost = np.array([[cost_to_kind(nm, PROJECTS[nm], k) for k in KINDS] for nm in names], dtype=float)
A_eq = [np.eye(m).flatten() if False else None] # placeholder removed below
A_eq, b_eq = [], []
for i in range(n):
row = np.zeros(n*m); row[i*m:(i+1)*m] = 1
A_eq.append(row); b_eq.append(1)
res = linprog(cost.flatten(), A_eq=A_eq, b_eq=b_eq, bounds=(0,1))
X = res.x.reshape(n, m)
plan = {}
for i, nm in enumerate(names):
j = int(np.argmax(X[i]))
plan[nm] = {"assigned_kind": KINDS[j], "cost": float(cost[i, j])}
print(json.dumps(plan, indent=2))
print("\ntotal bootstrap cost:", res.fun)