@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 +21 -0
- package/README.md +40 -0
- package/card_membership/Nargo.toml +8 -0
- package/card_membership/src/main.nr +106 -0
- package/dice_valid/Nargo.toml +8 -0
- package/dice_valid/src/main.nr +104 -0
- package/dist/index.cjs +304 -0
- package/dist/index.d.cts +89 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.js +265 -0
- package/move_along/Nargo.toml +8 -0
- package/move_along/fixtures/test_graph.json +13 -0
- package/move_along/src/main.nr +73 -0
- package/move_along/target/move_along.json +1 -0
- package/move_along/target/vk +0 -0
- package/move_along/target/vk_fields.json +1 -0
- package/package.json +42 -0
- package/scripts/build_all.sh +174 -0
- package/scripts/build_one.sh +11 -0
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,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,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
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -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 };
|