@zktable/circuits 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 zkTable contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # @zktable/circuits
2
+
3
+ Pre-built, reusable Noir (UltraHonk) circuits — one per zkTable ZK primitive —
4
+ plus TypeScript witness builders. Every game composes these; no game writes a
5
+ circuit.
6
+
7
+ ## Circuits
8
+
9
+ | Circuit | Module | Proves | Status |
10
+ |---|---|---|---|
11
+ | `simple_circuit` | — | `x != y` (M0 pipeline anchor) | ✅ built |
12
+ | `fib_chain` | — | fibonacci chain (M0 second fixture) | ✅ built |
13
+ | `board/move_along` | `board` | legal hidden edge move on a graph | M2 |
14
+ | `hidden/predicate` | `hidden` | Poseidon commitment + predicate | M2 |
15
+ | `dice/dice_valid` | `dice` | fair PRF-derived hidden roll | M6 |
16
+ | `deck/valid_shuffle`, `deck/card_membership` | `deck` | valid shuffle + hand membership | M6 |
17
+ | `sealed` | `sealed` | commit-reveal (Poseidon equality, no circuit needed) | M6 |
18
+
19
+ ## Building circuit artifacts
20
+
21
+ Requires `nargo` 1.0.0-beta.9 and `bb` v0.87.0 on PATH
22
+ (`$HOME/.nargo/bin:$HOME/.bb/bin`).
23
+
24
+ ```bash
25
+ scripts/build_one.sh <circuit> # e.g. board/move_along
26
+ scripts/build_all.sh # all circuits
27
+ ```
28
+
29
+ Each produces under `<circuit>/target/`: `proof` (14592 B), `public_inputs`,
30
+ `vk` (1760 B), plus `*_fields.json` variants.
31
+
32
+ **Proving is fixed to `--scheme ultra_honk --oracle_hash keccak`** — the
33
+ `keccak` oracle hash is required for the on-chain verifier. Do not change it.
34
+
35
+ Built `target/` artifacts are git-ignored; regenerate with the scripts above.
36
+
37
+ ## Witness builders (TypeScript)
38
+
39
+ `src/` exposes typed witness builders that turn a game move + player view into a
40
+ circuit's `Prover.toml` inputs, so `@zktable/core` never hand-writes witnesses.
@@ -0,0 +1,8 @@
1
+ [package]
2
+ name = "card_membership"
3
+ type = "bin"
4
+ authors = ["zkTable"]
5
+ compiler_version = ">=1.0.0"
6
+
7
+ [dependencies]
8
+ poseidon = { tag = "v0.2.0", git = "https://github.com/noir-lang/poseidon" }
@@ -0,0 +1,106 @@
1
+ // zkTable `deck` module - Coup-lite "prove-hold-or-bluff": prove a claimed
2
+ // character card really is one of the two cards in a player's hidden hand,
3
+ // without revealing the hand's contents (which card is which, or which slot
4
+ // is the claimed one).
5
+ //
6
+ // HONEST SIMPLIFICATION (PRD SS7.2 / M6.3 brief): this is deck v1, NOT full
7
+ // mental-poker/coSNARK dealing security. The hand is committed by a
8
+ // semi-honest dealer/orchestrator off-chain (see the coup-referee's `deal`
9
+ // phase and its module doc) - this circuit does not prove the deal itself
10
+ // came from a valid shuffle (that would be a `valid_shuffle` circuit, out of
11
+ // scope for v1, documented as future work). What IS proven, and IS
12
+ // load-bearing: given a player's two already-committed hand cards, the
13
+ // claimed character genuinely sits in that hand. This is exactly the "hold"
14
+ // half of Coup's "prove-hold-or-bluff" - a player who is bluffing (the
15
+ // claimed card is not in their hand) cannot produce a satisfying witness for
16
+ // ANY `held_index`, so on challenge they must fall back to revealing a real
17
+ // card instead (the referee's `reveal_card`) and lose influence.
18
+ //
19
+ // Hand size H = 2 (Coup: each player holds exactly 2 influence cards).
20
+ // Per-card commitments: c_i = Poseidon2(card_i, salt_i) (hash2), i in {0,1}.
21
+ //
22
+ // Public inputs (fixed order): [claimed_card, c_0, c_1] (3 fields).
23
+ // Private witness: card[2], salt[2], held_index (0 or 1, selects which hand
24
+ // slot equals the public claim).
25
+ //
26
+ // Proves:
27
+ // 1. BOTH hand cards open their public commitments: hash2(card[i], salt[i])
28
+ // == c_i for i in {0,1}. (Knowledge of the full, real hand contents -
29
+ // not just the claimed card - so a prover cannot fabricate a
30
+ // single-card "hand" that happens to match the claim.)
31
+ // 2. held_index in {0,1} (a real slot selector, not an out-of-range value).
32
+ // 3. The card at the selected slot equals the public claimed_card:
33
+ // card[held_index] == claimed_card. (Since H=2, this selection is done
34
+ // with a direct linear combination rather than a general selector
35
+ // gadget - see `select` below.)
36
+ //
37
+ // hash2 == Barretenberg/Noir Poseidon2 over 2 inputs, byte-identical to the
38
+ // off-chain `zktable-graph` tool (soroban-poseidon `poseidon2_hash`) and the
39
+ // on-chain referee's `poseidon2_hash2` - the same scheme `board`/`dice`
40
+ // already use in production.
41
+ use dep::poseidon::poseidon2::Poseidon2;
42
+
43
+ // Fixed hand size. Public-input layout below assumes H = 2.
44
+ global H: u32 = 2;
45
+
46
+ fn hash2(a: Field, b: Field) -> Field {
47
+ Poseidon2::hash([a, b], 2)
48
+ }
49
+
50
+ // Linear-combination select over exactly 2 elements: held_index == 0 -> a[0],
51
+ // held_index == 1 -> a[1]. Sound ONLY given the caller has already
52
+ // constrained held_index * (held_index - 1) == 0 (see `main` below) - this
53
+ // function performs no range check of its own.
54
+ fn select2(a: [Field; H], held_index: Field) -> Field {
55
+ a[0] + held_index * (a[1] - a[0])
56
+ }
57
+
58
+ pub fn main(
59
+ claimed_card: pub Field,
60
+ c: pub [Field; H],
61
+ card: [Field; H],
62
+ salt: [Field; H],
63
+ held_index: Field,
64
+ ) {
65
+ // 1. Both hand cards open their public commitments - proves knowledge of
66
+ // the REAL committed hand, not just a single fabricated card.
67
+ for i in 0..H {
68
+ assert(hash2(card[i], salt[i]) == c[i]);
69
+ }
70
+
71
+ // 2. held_index is a genuine slot selector (0 or 1), not an
72
+ // out-of-range/wrapped value.
73
+ assert(held_index * (held_index - 1) == 0);
74
+
75
+ // 3. The selected hand slot really holds the claimed character.
76
+ let selected = select2(card, held_index);
77
+ assert(selected == claimed_card);
78
+ }
79
+
80
+ #[test]
81
+ fn test_smoke() {
82
+ // Sanity: a commitment round-trips, and select2 picks the right slot.
83
+ let c0 = hash2(2, 11);
84
+ assert(c0 == hash2(2, 11));
85
+ assert(select2([2, 4], 0) == 2);
86
+ assert(select2([2, 4], 1) == 4);
87
+ }
88
+
89
+ #[test]
90
+ fn test_hold_valid() {
91
+ // Hand = [Duke(=1), Assassin(=3)], held_index = 1 -> claiming Assassin.
92
+ let card = [1, 3];
93
+ let salt = [11, 22];
94
+ let c = [hash2(card[0], salt[0]), hash2(card[1], salt[1])];
95
+ main(3, c, card, salt, 1);
96
+ }
97
+
98
+ #[test(should_fail)]
99
+ fn test_bluff_fails() {
100
+ // Hand does not contain the claimed character (Contessa = 4) at all -
101
+ // no held_index makes `select2(card, held_index) == claimed_card` hold.
102
+ let card = [1, 3];
103
+ let salt = [11, 22];
104
+ let c = [hash2(card[0], salt[0]), hash2(card[1], salt[1])];
105
+ main(4, c, card, salt, 0);
106
+ }
@@ -0,0 +1,8 @@
1
+ [package]
2
+ name = "dice_valid"
3
+ type = "bin"
4
+ authors = ["zkTable"]
5
+ compiler_version = ">=1.0.0"
6
+
7
+ [dependencies]
8
+ poseidon = { tag = "v0.2.0", git = "https://github.com/noir-lang/poseidon" }
@@ -0,0 +1,104 @@
1
+ // zkTable `dice` module - hidden, provably-fair dice for Liar's Dice.
2
+ //
3
+ // Proves a player's N=5 hidden dice are (1) committed, (2) valid faces in
4
+ // {1,...,6}, and (3) the canonical, in-circuit-bound reduction of a per-die
5
+ // hash derived from a PUBLIC shared seed and the player's id - so the roll is
6
+ // unforgeable: it is fixed the moment the seed is fixed, before any die value
7
+ // is revealed, and the circuit itself (not just an off-chain check) enforces
8
+ // that binding.
9
+ //
10
+ // Commitments: c_j = Poseidon2(d_j, salt_j) (hash2)
11
+ // Per-die hash: H_j = Poseidon2(Poseidon2(seed, player_id), j) (hash2 twice)
12
+ // Fair value: d_j = canonical_reduce_1_to_6(H_j)
13
+ //
14
+ // hash2 == Barretenberg/Noir Poseidon2 over 2 inputs, which the off-chain
15
+ // dice tool (soroban-poseidon `poseidon2_hash`) reproduces exactly - the same
16
+ // scheme the `board` module (move_along) already uses in production.
17
+ use dep::poseidon::poseidon2::Poseidon2;
18
+
19
+ // Fixed roll size and die sides. Public-input layout below assumes N = 5.
20
+ global N: u32 = 5;
21
+ global SIDES: Field = 6;
22
+
23
+ // floor((p-1)/6) for the BN254 scalar field
24
+ // p = 21888242871839275222246405745257275088548364400416034343698204186575808495617.
25
+ // This is the maximum possible integer value of the TRUE quotient
26
+ // q = H_j / 6 (integer division) for any H_j in [0, p-1]. It bounds the
27
+ // canonicality check in `constrain_fair_die` below.
28
+ global Q_MAX: Field = 3648040478639879203707734290876212514758060733402672390616367364429301415936;
29
+
30
+ // Q_MAX fits in 252 bits, and 2 * 2^252 < p (p has 254 bits), so a two-sided
31
+ // 252-bit range check on q is sound with no field-wraparound ambiguity - see
32
+ // `constrain_fair_die` and the report for the full argument.
33
+ global Q_BITS: u32 = 252;
34
+
35
+ fn hash2(a: Field, b: Field) -> Field {
36
+ Poseidon2::hash([a, b], 2)
37
+ }
38
+
39
+ // Enforce that `die` is the CANONICAL reduction of `h` into {1,...,SIDES}
40
+ // (SIDES fixed at 6): h == q*SIDES + r as integers, 0 <= r <= SIDES-1,
41
+ // die == r + 1. Field arithmetic alone (h == q*SIDES + r as a Field equation)
42
+ // admits SIDES possible (q, r) solutions - one per residue r in {0..5} - since
43
+ // SIDES is coprime to the prime field order p. Only one of those solutions has
44
+ // q in the "true" integer-division range [0, Q_MAX]; the other SIDES-1 wrap
45
+ // the field and land on q values that require far more than Q_BITS bits. The
46
+ // two-sided range check below (q bounded AND Q_MAX - q bounded, both to
47
+ // Q_BITS bits, with 2*2^Q_BITS < p) admits only the true, unwrapped solution.
48
+ fn constrain_fair_die(h: Field, q: Field, r: Field, die: Field) {
49
+ // r in {0,...,5}: direct product constraint (SIDES is a small compile-time
50
+ // constant, so no general range gadget is needed).
51
+ assert(r * (r - 1) * (r - 2) * (r - 3) * (r - 4) * (r - 5) == 0);
52
+ // Euclidean relation h = q*6 + r (checked as a Field equation).
53
+ assert(h == q * SIDES + r);
54
+ // Canonicality: pin q into the integer range [0, Q_MAX], ruling out every
55
+ // field-wrapped alternative (q, r) pair.
56
+ q.assert_max_bit_size::<Q_BITS>();
57
+ (Q_MAX - q).assert_max_bit_size::<Q_BITS>();
58
+ // Face value.
59
+ assert(die == r + 1);
60
+ }
61
+
62
+ pub fn main(
63
+ seed: pub Field,
64
+ player_id: pub Field,
65
+ c: pub [Field; N],
66
+ d: [Field; N],
67
+ salt: [Field; N],
68
+ q: [Field; N],
69
+ r: [Field; N],
70
+ ) {
71
+ // Bind every per-die hash to the public (seed, player_id) pair once.
72
+ let seed_player = hash2(seed, player_id);
73
+
74
+ for j in 0..N {
75
+ let dj = d[j];
76
+
77
+ // 1. Commitment: the hidden die and salt open the public commitment.
78
+ assert(hash2(dj, salt[j]) == c[j]);
79
+
80
+ // 2. Valid die: dj in {1,...,6} (direct product constraint).
81
+ assert((dj - 1) * (dj - 2) * (dj - 3) * (dj - 4) * (dj - 5) * (dj - 6) == 0);
82
+
83
+ // 3. Fair derivation: dj is the canonical reduction into [1,6] of the
84
+ // per-die hash H_j, which is itself recomputed in-circuit from the
85
+ // public seed, player_id, and loop index j. This is what makes the
86
+ // committed roll unforgeable: it is fully determined by (seed,
87
+ // player_id, j) before any salt or commitment is chosen.
88
+ let h = hash2(seed_player, j as Field);
89
+ constrain_fair_die(h, q[j], r[j], dj);
90
+ }
91
+ }
92
+
93
+ #[test]
94
+ fn test_smoke() {
95
+ // Sanity: a commitment round-trips, and the canonical reduction of a
96
+ // small concrete hash lands where expected. (Full end-to-end derivation
97
+ // is exercised by the Rust witness tool + `nargo execute` + `bb prove`,
98
+ // which control the seed and match the off-chain hash byte-for-byte.)
99
+ let c = hash2(7, 42);
100
+ assert(c == hash2(7, 42));
101
+
102
+ // h = 6*3 + 4 = 22 -> r = 4, die = 5.
103
+ constrain_fair_die(22, 3, 4, 5);
104
+ }
package/dist/index.cjs ADDED
@@ -0,0 +1,304 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ BoardGraph: () => BoardGraph,
34
+ BoardProver: () => BoardProver
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // src/graph.ts
39
+ var import_node_child_process = require("child_process");
40
+ var import_promises = require("fs/promises");
41
+ var import_node_os2 = __toESM(require("os"), 1);
42
+ var import_node_path2 = __toESM(require("path"), 1);
43
+ var import_node_util = require("util");
44
+
45
+ // src/paths.ts
46
+ var import_node_os = __toESM(require("os"), 1);
47
+ var import_node_path = __toESM(require("path"), 1);
48
+ var import_node_url = require("url");
49
+ var import_meta = {};
50
+ var here = import_node_path.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
51
+ var PACKAGE_ROOT = import_node_path.default.resolve(here, "..");
52
+ var DEFAULT_CIRCUIT_DIR = import_node_path.default.join(PACKAGE_ROOT, "move_along");
53
+ var CIRCUIT_PACKAGE_NAME = "move_along";
54
+ var DEFAULT_GRAPH_TOOLS_BIN = import_node_path.default.resolve(
55
+ PACKAGE_ROOT,
56
+ "../contracts/tools/graph-tools/target/release/zktable-graph"
57
+ );
58
+ var DEFAULT_NARGO_BIN = "nargo";
59
+ var DEFAULT_BB_BIN = "bb";
60
+ function toolEnv() {
61
+ const home = import_node_os.default.homedir();
62
+ const extraDirs = [import_node_path.default.join(home, ".nargo", "bin"), import_node_path.default.join(home, ".bb", "bin")];
63
+ const currentPath = process.env.PATH ?? "";
64
+ return {
65
+ ...process.env,
66
+ PATH: [...extraDirs, currentPath].filter(Boolean).join(import_node_path.default.delimiter)
67
+ };
68
+ }
69
+
70
+ // src/graph.ts
71
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
72
+ function edgeKey(from, to, ticket) {
73
+ return `${from}:${to}:${ticket}`;
74
+ }
75
+ function canonicalEdgeSet(edges, bidirectional) {
76
+ const set = /* @__PURE__ */ new Set();
77
+ for (const e of edges) {
78
+ set.add(edgeKey(e.from, e.to, e.ticket));
79
+ if (bidirectional) {
80
+ set.add(edgeKey(e.to, e.from, e.ticket));
81
+ }
82
+ }
83
+ return set;
84
+ }
85
+ var BoardGraph = class {
86
+ data;
87
+ graphToolsBin;
88
+ edges;
89
+ constructor(data, opts) {
90
+ this.data = data;
91
+ this.graphToolsBin = opts?.graphToolsBin ?? DEFAULT_GRAPH_TOOLS_BIN;
92
+ this.edges = canonicalEdgeSet(data.edges, data.bidirectional ?? true);
93
+ }
94
+ /** Pure, local check: is (from, to, ticket) a legal edge of this graph? */
95
+ hasEdge(from, to, ticket) {
96
+ return this.edges.has(edgeKey(from, to, ticket));
97
+ }
98
+ /** Edge-tree root (hex), via `zktable-graph root`. */
99
+ async root() {
100
+ const dir = await (0, import_promises.mkdtemp)(import_node_path2.default.join(import_node_os2.default.tmpdir(), "zktable-graph-root-"));
101
+ try {
102
+ const graphPath = import_node_path2.default.join(dir, "graph.json");
103
+ await (0, import_promises.writeFile)(graphPath, JSON.stringify(this.data));
104
+ const { stdout } = await execFileAsync(this.graphToolsBin, ["root", "--graph", graphPath], {
105
+ env: toolEnv()
106
+ });
107
+ return stdout.trim();
108
+ } finally {
109
+ await (0, import_promises.rm)(dir, { recursive: true, force: true });
110
+ }
111
+ }
112
+ /** Poseidon2(node, salt) (hex), via `zktable-graph commit`. */
113
+ async commit(node, salt) {
114
+ const { stdout } = await execFileAsync(
115
+ this.graphToolsBin,
116
+ ["commit", "--node", String(node), "--salt", salt.toString()],
117
+ { env: toolEnv() }
118
+ );
119
+ return stdout.trim();
120
+ }
121
+ };
122
+
123
+ // src/prover.ts
124
+ var import_node_child_process2 = require("child_process");
125
+ var import_promises2 = require("fs/promises");
126
+ var import_node_os3 = __toESM(require("os"), 1);
127
+ var import_node_path3 = __toESM(require("path"), 1);
128
+ var import_node_util2 = require("util");
129
+ var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
130
+ async function exists(p) {
131
+ try {
132
+ await (0, import_promises2.access)(p);
133
+ return true;
134
+ } catch {
135
+ return false;
136
+ }
137
+ }
138
+ var BoardProver = class {
139
+ circuitDir;
140
+ graphToolsBin;
141
+ nargoBin;
142
+ bbBin;
143
+ workDir;
144
+ env;
145
+ constructor(opts = {}) {
146
+ this.circuitDir = opts.circuitDir ?? DEFAULT_CIRCUIT_DIR;
147
+ this.graphToolsBin = opts.graphToolsBin ?? DEFAULT_GRAPH_TOOLS_BIN;
148
+ this.nargoBin = opts.nargoBin ?? DEFAULT_NARGO_BIN;
149
+ this.bbBin = opts.bbBin ?? DEFAULT_BB_BIN;
150
+ this.workDir = opts.workDir ?? import_node_os3.default.tmpdir();
151
+ this.env = toolEnv();
152
+ }
153
+ get targetDir() {
154
+ return import_node_path3.default.join(this.circuitDir, "target");
155
+ }
156
+ get bytecodePath() {
157
+ return import_node_path3.default.join(this.targetDir, `${CIRCUIT_PACKAGE_NAME}.json`);
158
+ }
159
+ get vkPath() {
160
+ return import_node_path3.default.join(this.targetDir, "vk");
161
+ }
162
+ /** Compile the circuit once if `target/move_along.json` is missing. */
163
+ async ensureCompiled() {
164
+ if (await exists(this.bytecodePath)) return;
165
+ await execFileAsync2(this.nargoBin, ["compile"], { cwd: this.circuitDir, env: this.env });
166
+ }
167
+ /** Generate the verification key once if `target/vk` is missing. */
168
+ async ensureVk() {
169
+ await this.ensureCompiled();
170
+ if (await exists(this.vkPath)) return;
171
+ await execFileAsync2(
172
+ this.bbBin,
173
+ [
174
+ "write_vk",
175
+ "--scheme",
176
+ "ultra_honk",
177
+ "--oracle_hash",
178
+ "keccak",
179
+ "--bytecode_path",
180
+ this.bytecodePath,
181
+ "--output_path",
182
+ this.targetDir,
183
+ "--output_format",
184
+ "bytes_and_fields"
185
+ ],
186
+ { env: this.env }
187
+ );
188
+ }
189
+ /**
190
+ * Full pipeline: write graph -> `zktable-graph witness` -> `nargo execute`
191
+ * -> `bb prove`. Runs entirely inside an isolated temp dir; never touches
192
+ * `move_along/Prover.toml` or `move_along/target/`.
193
+ */
194
+ async prove(graph, move) {
195
+ await this.ensureCompiled();
196
+ const dir = await (0, import_promises2.mkdtemp)(import_node_path3.default.join(this.workDir, "zktable-prove-"));
197
+ try {
198
+ const graphPath = import_node_path3.default.join(dir, "graph.json");
199
+ const proverTomlPath = import_node_path3.default.join(dir, "Prover");
200
+ const witnessJsonPath = import_node_path3.default.join(dir, "witness.json");
201
+ const witnessOutPath = import_node_path3.default.join(dir, "witness");
202
+ await (0, import_promises2.writeFile)(graphPath, JSON.stringify(graph));
203
+ await execFileAsync2(
204
+ this.graphToolsBin,
205
+ [
206
+ "witness",
207
+ "--graph",
208
+ graphPath,
209
+ "--from",
210
+ String(move.from),
211
+ "--to",
212
+ String(move.to),
213
+ "--ticket",
214
+ String(move.ticket),
215
+ "--salt-old",
216
+ move.saltOld.toString(),
217
+ "--salt-new",
218
+ move.saltNew.toString(),
219
+ "--prover",
220
+ `${proverTomlPath}.toml`,
221
+ "--json",
222
+ witnessJsonPath
223
+ ],
224
+ { env: this.env }
225
+ );
226
+ const witness = JSON.parse(await (0, import_promises2.readFile)(witnessJsonPath, "utf8"));
227
+ await execFileAsync2(this.nargoBin, ["execute", "--prover-name", proverTomlPath, witnessOutPath], {
228
+ cwd: this.circuitDir,
229
+ env: this.env
230
+ });
231
+ await execFileAsync2(
232
+ this.bbBin,
233
+ [
234
+ "prove",
235
+ "--scheme",
236
+ "ultra_honk",
237
+ "--oracle_hash",
238
+ "keccak",
239
+ "--bytecode_path",
240
+ this.bytecodePath,
241
+ "--witness_path",
242
+ `${witnessOutPath}.gz`,
243
+ "--output_path",
244
+ dir,
245
+ "--output_format",
246
+ "bytes_and_fields"
247
+ ],
248
+ { env: this.env }
249
+ );
250
+ const proof = new Uint8Array(await (0, import_promises2.readFile)(import_node_path3.default.join(dir, "proof")));
251
+ const publicInputs = new Uint8Array(await (0, import_promises2.readFile)(import_node_path3.default.join(dir, "public_inputs")));
252
+ return {
253
+ proof,
254
+ publicInputs,
255
+ cOld: witness.c_old,
256
+ cNew: witness.c_new,
257
+ root: witness.root,
258
+ ticket: move.ticket
259
+ };
260
+ } finally {
261
+ await (0, import_promises2.rm)(dir, { recursive: true, force: true });
262
+ }
263
+ }
264
+ /** Off-chain check via `bb verify`. */
265
+ async verifyLocally(proof) {
266
+ await this.ensureVk();
267
+ const dir = await (0, import_promises2.mkdtemp)(import_node_path3.default.join(this.workDir, "zktable-verify-"));
268
+ try {
269
+ const proofPath = import_node_path3.default.join(dir, "proof");
270
+ const publicInputsPath = import_node_path3.default.join(dir, "public_inputs");
271
+ await (0, import_promises2.writeFile)(proofPath, proof.proof);
272
+ await (0, import_promises2.writeFile)(publicInputsPath, proof.publicInputs);
273
+ try {
274
+ await execFileAsync2(
275
+ this.bbBin,
276
+ [
277
+ "verify",
278
+ "--scheme",
279
+ "ultra_honk",
280
+ "--oracle_hash",
281
+ "keccak",
282
+ "--proof_path",
283
+ proofPath,
284
+ "--vk_path",
285
+ this.vkPath,
286
+ "--public_inputs_path",
287
+ publicInputsPath
288
+ ],
289
+ { env: this.env }
290
+ );
291
+ return true;
292
+ } catch {
293
+ return false;
294
+ }
295
+ } finally {
296
+ await (0, import_promises2.rm)(dir, { recursive: true, force: true });
297
+ }
298
+ }
299
+ };
300
+ // Annotate the CommonJS export names for ESM import in node:
301
+ 0 && (module.exports = {
302
+ BoardGraph,
303
+ BoardProver
304
+ });
@@ -0,0 +1,89 @@
1
+ type Ticket = 0 | 1 | 2;
2
+ type GraphEdge = {
3
+ from: number;
4
+ to: number;
5
+ ticket: Ticket;
6
+ };
7
+ type GraphData = {
8
+ nodes: number[];
9
+ edges: GraphEdge[];
10
+ /** Expand each edge to both directions. Defaults to true. */
11
+ bidirectional?: boolean;
12
+ };
13
+ type BoardMove = {
14
+ from: number;
15
+ to: number;
16
+ ticket: Ticket;
17
+ saltOld: bigint;
18
+ saltNew: bigint;
19
+ };
20
+ type BoardProof = {
21
+ /** UltraHonk proof bytes (14592 bytes). */
22
+ proof: Uint8Array;
23
+ /** c_old | c_new | ticket | root, 32 bytes each = 128 bytes total. */
24
+ publicInputs: Uint8Array;
25
+ cOld: string;
26
+ cNew: string;
27
+ root: string;
28
+ ticket: Ticket;
29
+ };
30
+
31
+ /**
32
+ * A public transit graph. Wraps the `zktable-graph` Rust CLI for the
33
+ * hash-dependent operations (edge-tree root, Poseidon commitments) and does
34
+ * pure, local edge-membership checks in TS.
35
+ */
36
+ declare class BoardGraph {
37
+ readonly data: GraphData;
38
+ private readonly graphToolsBin;
39
+ private readonly edges;
40
+ constructor(data: GraphData, opts?: {
41
+ graphToolsBin?: string;
42
+ });
43
+ /** Pure, local check: is (from, to, ticket) a legal edge of this graph? */
44
+ hasEdge(from: number, to: number, ticket: Ticket): boolean;
45
+ /** Edge-tree root (hex), via `zktable-graph root`. */
46
+ root(): Promise<string>;
47
+ /** Poseidon2(node, salt) (hex), via `zktable-graph commit`. */
48
+ commit(node: number, salt: bigint): Promise<string>;
49
+ }
50
+
51
+ type BoardProverOptions = {
52
+ circuitDir?: string;
53
+ graphToolsBin?: string;
54
+ nargoBin?: string;
55
+ bbBin?: string;
56
+ workDir?: string;
57
+ };
58
+ /**
59
+ * Orchestrates the full `board` module pipeline against real tools: writes
60
+ * the graph, builds the witness via `zktable-graph`, executes the circuit
61
+ * via `nargo`, and proves/verifies via `bb`. No mocks — every call produces
62
+ * or checks a real UltraHonk proof.
63
+ */
64
+ declare class BoardProver {
65
+ private readonly circuitDir;
66
+ private readonly graphToolsBin;
67
+ private readonly nargoBin;
68
+ private readonly bbBin;
69
+ private readonly workDir;
70
+ private readonly env;
71
+ constructor(opts?: BoardProverOptions);
72
+ private get targetDir();
73
+ private get bytecodePath();
74
+ private get vkPath();
75
+ /** Compile the circuit once if `target/move_along.json` is missing. */
76
+ private ensureCompiled;
77
+ /** Generate the verification key once if `target/vk` is missing. */
78
+ private ensureVk;
79
+ /**
80
+ * Full pipeline: write graph -> `zktable-graph witness` -> `nargo execute`
81
+ * -> `bb prove`. Runs entirely inside an isolated temp dir; never touches
82
+ * `move_along/Prover.toml` or `move_along/target/`.
83
+ */
84
+ prove(graph: GraphData, move: BoardMove): Promise<BoardProof>;
85
+ /** Off-chain check via `bb verify`. */
86
+ verifyLocally(proof: BoardProof): Promise<boolean>;
87
+ }
88
+
89
+ export { BoardGraph, type BoardMove, type BoardProof, BoardProver, type BoardProverOptions, type GraphData, type GraphEdge, type Ticket };