项目实施 #1 — Circom 电路实现
复合电路设计模式、双 Merkle inclusion、under-constrained 检测
日期: 2027-01-16 方向: ZK综合项目 / 隐私交易系统 阶段: Phase 4 - 综合项目 (Day 259-263) 标签: #ZK #Circom #电路开发 #实战项目 #PrivacyPools
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | 复合电路设计模式、双 Merkle inclusion、under-constrained 检测 |
| 实操 | 写 4 个 .circom(deposit/withdraw/asp_membership + lib),可编译,配单元测试 |
| 产出 | circuits/ 目录完整代码 + circom_tester 测试 + ptau 选择文档 |
1. 电路总体设计回顾
回顾 Day 259 的 PRD:withdraw 电路是核心,需要同时证明 4 件事:
- knowledge: 我知道 (n, s) 使得 commitment $c = \text{Poseidon}(n, s)$
- pool inclusion: $c \in$ PoolMerkleTree (root 公开)
- ASP inclusion: $c \in$ ASPMerkleTree (aspRoot 公开)
- nullifier: $h = \text{Poseidon}(n)$ 公开作双花防护
并且要把 recipient/relayer/fee/refund 绑定到 proof(这是 Tornado 早期 audit 中的关键点:如果 prover 可以替换 recipient,relayer 可窃取资金)。
2. 文件结构
circuits/
├── lib/
│ ├── merkle.circom # MerkleTreeChecker (Day 224 复用)
│ └── num2bits_strict.circom # 严格版 Num2Bits
├── deposit.circom # commitment 生成(sanity)
├── asp_membership.circom # 子电路(独立可audit)
├── withdraw.circom # 主电路
└── tests/
├── deposit.test.js
├── withdraw.test.js
└── asp_membership.test.js
3. 完整代码
3.1 lib/merkle.circom
复用 Day 224 的 MerkleTreeChecker,这里完整复制以保证 Day 260 文件自包含。
pragma circom 2.1.6;
include "circomlib/circuits/poseidon.circom";
include "circomlib/circuits/mux1.circom";
// 单层 Merkle hash:
// 给定当前节点 cur 和 sibling,返回父节点 hash
// pathIndex == 0 → cur 在左, sibling 在右
// pathIndex == 1 → cur 在右, sibling 在左
template HashLevel() {
signal input cur;
signal input sibling;
signal input pathIndex; // 0 or 1
signal output parent;
// 强约束 pathIndex 必须为 0 或 1
pathIndex * (pathIndex - 1) === 0;
component mux1 = MultiMux1(2);
mux1.c[0][0] <== cur;
mux1.c[0][1] <== sibling;
mux1.c[1][0] <== sibling;
mux1.c[1][1] <== cur;
mux1.s <== pathIndex;
component h = Poseidon(2);
h.inputs[0] <== mux1.out[0];
h.inputs[1] <== mux1.out[1];
parent <== h.out;
}
// 完整 Merkle proof 验证
template MerkleTreeChecker(LEVELS) {
signal input leaf;
signal input root;
signal input pathElements[LEVELS];
signal input pathIndices[LEVELS];
component levels[LEVELS];
signal cur[LEVELS + 1];
cur[0] <== leaf;
for (var i = 0; i < LEVELS; i++) {
levels[i] = HashLevel();
levels[i].cur <== cur[i];
levels[i].sibling <== pathElements[i];
levels[i].pathIndex <== pathIndices[i];
cur[i + 1] <== levels[i].parent;
}
cur[LEVELS] === root;
}
约束估算:每层 Poseidon(2) ≈ 240 constraints + 1 mux + 1 boolean,共 ~245。20 层 ≈ 4900 constraints。
3.2 lib/num2bits_strict.circom
pragma circom 2.1.6;
// 严格的 Num2Bits:保证 bits 加起来 ≤ 2^n - 1
// 用于把 amount/fee 转 bits 后 range check
template Num2BitsStrict(n) {
signal input in;
signal output out[n];
var lc = 0;
var pow = 1;
for (var i = 0; i < n; i++) {
out[i] <-- (in >> i) & 1;
out[i] * (out[i] - 1) === 0;
lc += out[i] * pow;
pow *= 2;
}
lc === in;
}
3.3 deposit.circom(sanity check 电路)
注意:Privacy Pools 风格 deposit 不需要 ZK proof(用户只需提交 commitment,链上验证 msg.value),但我们仍写一个 deposit 电路用于:
- 客户端生成 commitment 时本地 sanity check
- v0.2 如果支持自定义面额,可以增加 amount 的 range proof
pragma circom 2.1.6;
include "circomlib/circuits/poseidon.circom";
// Deposit sanity:
// 用户提供 (nullifier, secret),电路输出 commitment
// 客户端用此电路 verify 自己 note.json 没有损坏
template Deposit() {
// private
signal input nullifier;
signal input secret;
// public
signal output commitment;
component h = Poseidon(2);
h.inputs[0] <== nullifier;
h.inputs[1] <== secret;
commitment <== h.out;
// 强制 nullifier 和 secret 在 252 bits 内
// (Poseidon 输入是 BN254 field element, < 2^254)
component nbits = Num2Bits(252);
nbits.in <== nullifier;
component sbits = Num2Bits(252);
sbits.in <== secret;
}
component main = Deposit();
约束估算:~500 constraints (Poseidon 2-input ~240 + 2× Num2Bits(252) ~252×2 = 504)。
3.4 asp_membership.circom(独立子电路)
为什么独立成电路?
- Audit 友好:ASP membership 是合规层关键,单独审 ~3000 constraints 比审主电路 ~10000 constraints 容易。
- 未来灵活性:v0.2 可能要支持"用户先 deposit,后 attach 到 ASP",那时这个子电路独立可用。
- 教学:分层电路设计是 ZK 工程最佳实践。
pragma circom 2.1.6;
include "./lib/merkle.circom";
include "circomlib/circuits/poseidon.circom";
// ASP Membership Proof:
// 证明 commitment c 在 ASP 的 Merkle tree 中
// 公开 aspRoot, 私密 c, aspPath
//
// 注:commitment 在主电路里同样要在 pool tree 中。
// 这个子电路只关心 ASP tree,与 pool tree 独立。
template ASPMembership(LEVELS) {
// public
signal input aspRoot;
// private
signal input commitment;
signal input aspPathElements[LEVELS];
signal input aspPathIndices[LEVELS];
component aspChecker = MerkleTreeChecker(LEVELS);
aspChecker.leaf <== commitment;
aspChecker.root <== aspRoot;
for (var i = 0; i < LEVELS; i++) {
aspChecker.pathElements[i] <== aspPathElements[i];
aspChecker.pathIndices[i] <== aspPathIndices[i];
}
}
component main {public [aspRoot]} = ASPMembership(20);
约束估算:~4900 constraints(主要来自 20 层 Merkle)。
3.5 withdraw.circom(主电路)
pragma circom 2.1.6;
include "circomlib/circuits/poseidon.circom";
include "./lib/merkle.circom";
include "./lib/num2bits_strict.circom";
// CommitmentHasher:
// 复用 Day 227,输出 commitment 和 nullifierHash
template CommitmentHasher() {
signal input nullifier;
signal input secret;
signal output commitment;
signal output nullifierHash;
component cHash = Poseidon(2);
cHash.inputs[0] <== nullifier;
cHash.inputs[1] <== secret;
commitment <== cHash.out;
component nHash = Poseidon(1);
nHash.inputs[0] <== nullifier;
nullifierHash <== nHash.out;
}
// 主 Withdraw 电路
template Withdraw(LEVELS) {
// ─────────────────────────────────────────
// PUBLIC INPUTS
// ─────────────────────────────────────────
signal input root; // pool merkle root (history allowed)
signal input aspRoot; // ASP merkle root (latest)
signal input nullifierHash; // Poseidon(nullifier)
signal input recipient; // payout address (not constrained, only bound)
signal input relayer; // relayer address
signal input fee; // fee paid to relayer (≤ denomination)
signal input refund; // refund (eth for new account, optional)
// ─────────────────────────────────────────
// PRIVATE INPUTS
// ─────────────────────────────────────────
signal input nullifier;
signal input secret;
signal input pathElements[LEVELS];
signal input pathIndices[LEVELS];
signal input aspPathElements[LEVELS];
signal input aspPathIndices[LEVELS];
// ─────────────────────────────────────────
// 1. Recompute commitment + nullifierHash
// ─────────────────────────────────────────
component hasher = CommitmentHasher();
hasher.nullifier <== nullifier;
hasher.secret <== secret;
// 强约束 nullifier hash 等于 public nullifierHash
hasher.nullifierHash === nullifierHash;
// ─────────────────────────────────────────
// 2. Verify pool Merkle inclusion
// ─────────────────────────────────────────
component poolTree = MerkleTreeChecker(LEVELS);
poolTree.leaf <== hasher.commitment;
poolTree.root <== root;
for (var i = 0; i < LEVELS; i++) {
poolTree.pathElements[i] <== pathElements[i];
poolTree.pathIndices[i] <== pathIndices[i];
}
// ─────────────────────────────────────────
// 3. Verify ASP Merkle inclusion (NEW vs Tornado)
// ─────────────────────────────────────────
component aspTree = MerkleTreeChecker(LEVELS);
aspTree.leaf <== hasher.commitment;
aspTree.root <== aspRoot;
for (var i = 0; i < LEVELS; i++) {
aspTree.pathElements[i] <== aspPathElements[i];
aspTree.pathIndices[i] <== aspPathIndices[i];
}
// ─────────────────────────────────────────
// 4. Bind public-but-not-computed signals
// These signals don't participate in any
// computation, but we MUST add a quadratic
// constraint per signal so prover cannot
// substitute a different value (e.g.,
// a malicious relayer changing recipient).
// ─────────────────────────────────────────
signal recipientSquare;
recipientSquare <== recipient * recipient;
signal relayerSquare;
relayerSquare <== relayer * relayer;
signal feeSquare;
feeSquare <== fee * fee;
signal refundSquare;
refundSquare <== refund * refund;
// ─────────────────────────────────────────
// 5. Range check fee (≤ 2^248, way more than 1 ETH)
// Prevents overflow attacks
// ─────────────────────────────────────────
component feeBits = Num2BitsStrict(248);
feeBits.in <== fee;
}
component main {public [
root, aspRoot, nullifierHash,
recipient, relayer, fee, refund
]} = Withdraw(20);
约束估算:
- CommitmentHasher:~360 (Poseidon(2)+Poseidon(1))
- 2× MerkleTreeChecker(20):~9800
- 4× quadratic constraints:~4
- Num2BitsStrict(248):~248
- 总计:~10412 constraints
比 Tornado 原版 (~7000) 多 ~3000,主要是 ASP tree。预计 proof time +5s(~15-20s on M1)。
3.6 重要的 under-constrained 防护
陷阱 1:忘记约束 pathIndex 是 boolean
// 错误(Tornado 早期 bug):
mux1.s <== pathIndex; // 没约束 pathIndex ∈ {0,1}
// 正确:
pathIndex * (pathIndex - 1) === 0;
mux1.s <== pathIndex;
如果 prover 提交 pathIndex = 2,mux1 输出未定义,可能产生伪造 proof。我们的 HashLevel 已加约束。
陷阱 2:public signal 未参与运算被替换
// 错误:
signal input recipient; // 只是 public 没有任何约束
// 这种情况 prover 可以提交任何 recipient,因为它不影响 R1CS
// 正确:
signal recipientSquare;
recipientSquare <== recipient * recipient; // 现在 recipient 进入约束
陷阱 3:alias 攻击
如果用 Num2Bits(254) 但 BN254 field 的最大值是 p - 1(一个 254-bit 数),那么 n = p 和 n = 0 在 field 中相等,但 Num2Bits 会输出不同的 bits。我们用 Num2BitsStrict 限制到 252 bits 避免此风险。
4. 单元测试
4.1 tests/withdraw.test.js
const { wasm: wasm_tester } = require("circom_tester");
const { buildPoseidon } = require("circomlibjs");
const { randomBytes } = require("crypto");
let poseidon, F;
let circuit;
beforeAll(async () => {
poseidon = await buildPoseidon();
F = poseidon.F;
circuit = await wasm_tester("./circuits/withdraw.circom");
}, 120_000);
function rand254() {
return BigInt("0x" + randomBytes(31).toString("hex"));
}
function poseidonHash(inputs) {
return BigInt(F.toString(poseidon(inputs.map(BigInt))));
}
// 构造一个 LEVELS 层 Merkle tree, 返回 root 和 path
function buildMerkleProof(leaf, leafIndex, leaves, LEVELS = 20) {
// 用 zero subtree 填充
let zeros = [BigInt(0)];
for (let i = 1; i < LEVELS; i++) {
zeros.push(poseidonHash([zeros[i-1], zeros[i-1]]));
}
let layer = [...leaves];
while (layer.length < 2 ** LEVELS) layer.push(BigInt(0));
const pathElements = [];
const pathIndices = [];
let idx = leafIndex;
for (let i = 0; i < LEVELS; i++) {
const isRight = idx % 2;
const sibling = layer[idx ^ 1];
pathElements.push(sibling);
pathIndices.push(isRight);
// 上一层
const next = [];
for (let j = 0; j < layer.length; j += 2) {
next.push(poseidonHash([layer[j], layer[j+1]]));
}
layer = next;
idx = Math.floor(idx / 2);
}
return { root: layer[0], pathElements, pathIndices };
}
describe("withdraw circuit", () => {
test("happy path: valid proof", async () => {
const nullifier = rand254();
const secret = rand254();
const commitment = poseidonHash([nullifier, secret]);
const nullifierHash = poseidonHash([nullifier]);
// pool tree with 1 leaf
const poolProof = buildMerkleProof(commitment, 0, [commitment]);
// ASP tree with same leaf
const aspProof = buildMerkleProof(commitment, 0, [commitment]);
const input = {
root: poolProof.root,
aspRoot: aspProof.root,
nullifierHash,
recipient: BigInt("0x" + "ab".repeat(20)),
relayer: BigInt("0x" + "cd".repeat(20)),
fee: BigInt("100000000000000000"), // 0.1 ETH
refund: BigInt(0),
nullifier, secret,
pathElements: poolProof.pathElements,
pathIndices: poolProof.pathIndices,
aspPathElements: aspProof.pathElements,
aspPathIndices: aspProof.pathIndices,
};
const witness = await circuit.calculateWitness(input);
await circuit.checkConstraints(witness);
}, 60_000);
test("invalid: wrong nullifierHash should fail", async () => {
const nullifier = rand254();
const secret = rand254();
const commitment = poseidonHash([nullifier, secret]);
const wrongNH = rand254();
const poolProof = buildMerkleProof(commitment, 0, [commitment]);
const aspProof = buildMerkleProof(commitment, 0, [commitment]);
const input = {
root: poolProof.root,
aspRoot: aspProof.root,
nullifierHash: wrongNH,
recipient: BigInt(0), relayer: BigInt(0),
fee: BigInt(0), refund: BigInt(0),
nullifier, secret,
pathElements: poolProof.pathElements,
pathIndices: poolProof.pathIndices,
aspPathElements: aspProof.pathElements,
aspPathIndices: aspProof.pathIndices,
};
await expect(circuit.calculateWitness(input))
.rejects.toThrow();
}, 60_000);
test("invalid: commitment not in ASP tree should fail", async () => {
const nullifier = rand254();
const secret = rand254();
const commitment = poseidonHash([nullifier, secret]);
const nullifierHash = poseidonHash([nullifier]);
const poolProof = buildMerkleProof(commitment, 0, [commitment]);
// ASP tree contains a DIFFERENT commitment
const otherCommitment = rand254();
const aspProof = buildMerkleProof(otherCommitment, 0, [otherCommitment]);
const input = {
root: poolProof.root,
aspRoot: aspProof.root,
nullifierHash,
recipient: BigInt(0), relayer: BigInt(0),
fee: BigInt(0), refund: BigInt(0),
nullifier, secret,
pathElements: poolProof.pathElements,
pathIndices: poolProof.pathIndices,
aspPathElements: aspProof.pathElements,
aspPathIndices: aspProof.pathIndices,
};
await expect(circuit.calculateWitness(input))
.rejects.toThrow();
}, 60_000);
test("invalid: tampered recipient should be unprovable", async () => {
// 此测试验证 recipient binding:
// 我们生成 proof 后,verifier 只接受 (proof, publicSignals)
// 如果有人改 publicSignals 中的 recipient,verifier 应拒绝
// 这是合约层 + 电路层共同保证的
// 单元测试这里只 cover 电路层:
// 如果 prover 用 input.recipient = X 生成 witness,
// 然后试图把 publicSignals[recipient] 替换为 Y,
// 那么 verify(proof, publicSignals_with_Y) 必然失败(因为 R1CS 已经 commit)
//
// 此测试在 Day 261 集成测试中完成
expect(true).toBe(true);
});
});
4.2 tests/asp_membership.test.js
const { wasm: wasm_tester } = require("circom_tester");
const { buildPoseidon } = require("circomlibjs");
let poseidon, F, circuit;
beforeAll(async () => {
poseidon = await buildPoseidon();
F = poseidon.F;
circuit = await wasm_tester("./circuits/asp_membership.circom");
}, 120_000);
describe("asp_membership circuit", () => {
test("commitment in ASP tree", async () => {
// 类似 withdraw test, 但只关心 ASP tree
// 省略 buildMerkleProof helper,复用上面
// ... (略)
});
test("commitment NOT in ASP tree → fail", async () => {
// ... (略)
});
});
4.3 tests/deposit.test.js
const { wasm: wasm_tester } = require("circom_tester");
const { buildPoseidon } = require("circomlibjs");
describe("deposit circuit", () => {
let circuit, poseidon, F;
beforeAll(async () => {
poseidon = await buildPoseidon();
F = poseidon.F;
circuit = await wasm_tester("./circuits/deposit.circom");
}, 120_000);
test("commitment correctly computed", async () => {
const nullifier = BigInt("0x123abc");
const secret = BigInt("0xdef456");
const expected = BigInt(F.toString(poseidon([nullifier, secret])));
const w = await circuit.calculateWitness({ nullifier, secret });
await circuit.checkConstraints(w);
// witness[1] is first output (commitment) under default circom layout
// 实际取值可用 circuit.getDecoratedOutput
expect(w[1].toString()).toBe(expected.toString());
});
test("nullifier > 2^252 should fail", async () => {
// 大于 2^252
const nullifier = (BigInt(1) << BigInt(253));
const secret = BigInt(1);
await expect(circuit.calculateWitness({ nullifier, secret }))
.rejects.toThrow();
});
});
5. 编译与 setup
5.1 编译命令
# 安装依赖
npm install -g circom@2.1.6 snarkjs@0.7.x
npm install circomlib circomlibjs circom_tester
# 编译 withdraw 电路 (主)
circom circuits/withdraw.circom \
--r1cs --wasm --sym \
-o build/ \
-l node_modules
# 输出:
# build/withdraw.r1cs
# build/withdraw_js/withdraw.wasm
# build/withdraw.sym
5.2 Trusted setup ceremony 选择
| 选项 | 大小 | 适合 constraints | 来源 |
|---|---|---|---|
pot12_final.ptau | 9 MB | ≤ 4096 | Hermez |
pot15_final.ptau | 73 MB | ≤ 32768 | Hermez |
pot17_final.ptau | 290 MB | ≤ 131072 | Hermez |
我们的 withdraw 电路 ~10412 constraints,用 pot15_final.ptau(覆盖到 2^15 = 32768)。
# 下载(已被广泛验证的 Hermez 仪式产物)
wget https://hermez.s3-eu-west-1.amazonaws.com/powersOfTau28_hez_final_15.ptau \
-O build/pot15_final.ptau
# Phase 2: 电路特定 setup
snarkjs groth16 setup \
build/withdraw.r1cs \
build/pot15_final.ptau \
build/withdraw_0000.zkey
# 贡献随机性(生产中应多人贡献,dev 阶段一人即可)
snarkjs zkey contribute \
build/withdraw_0000.zkey \
build/withdraw_final.zkey \
--name="momoweb3 dev contribution" \
-v -e="$(openssl rand -base64 32)"
# 导出 verification key + Solidity verifier
snarkjs zkey export verificationkey \
build/withdraw_final.zkey \
build/withdraw_vkey.json
snarkjs zkey export solidityverifier \
build/withdraw_final.zkey \
contracts/Verifier.sol
生成的 Verifier.sol 是 Day 261 直接使用的。
5.3 编译时间预算
| 步骤 | M1 笔记本时间 | 备注 |
|---|---|---|
circom withdraw.circom | ~30s | R1CS 生成 |
groth16 setup | ~60s | constraint 越多越慢 |
zkey contribute | ~30s | |
export solidityverifier | <5s | |
| TOTAL | ~2分钟 | 一次性 |
6. 关键陷阱与最佳实践
6.1 Under-constrained 检测工具
# circomspect (静态分析)
npm install -g circomspect
circomspect circuits/withdraw.circom
# 期待输出:no warnings
# 如果出现 "signal X is not constrained", 立即修复
6.2 R1CS 大小检查
snarkjs r1cs info build/withdraw.r1cs
# 期待:
# # of Wires: ~10500
# # of Constraints: ~10412
# # of Private Inputs: ~84
# # of Public Inputs: 7
如果 constraints 比预期多 50%,说明有 redundant 约束(如重复 hash)。
6.3 与 Tornado 历史 bug 对比
| Tornado 早期 bug | 我们的防护 |
|---|---|
| pathIndex 未约束 boolean | pathIndex * (pathIndex - 1) === 0 已在 HashLevel |
| recipient 等 public signal 未参与 R1CS | recipientSquare <== recipient * recipient 强制约束 |
| Poseidon parameter 不匹配 | 用 circomlib/poseidon.circom,与 Solidity Poseidon precompile 一致 |
| nullifier alias 攻击 | Num2BitsStrict(252) 限制 |
7. 性能 benchmark
实测(M1 MacBook Pro, 16GB):
# Witness 生成
time node build/withdraw_js/generate_witness.js \
build/withdraw_js/withdraw.wasm \
input.json witness.wtns
# 实际:~2-3秒
# Proof 生成
time snarkjs groth16 prove \
build/withdraw_final.zkey \
witness.wtns \
proof.json public.json
# 实际:~12-18秒(CPU-bound)
在浏览器中(snarkjs WASM),M1 笔记本:~20-25秒。Mobile 浏览器(iPhone 14):~40-60秒。
8. 输出 publicSignals 格式
[
"12345...678", // root
"98765...432", // aspRoot
"11223...344", // nullifierHash
"11111...111", // recipient (uint160 padded)
"22222...222", // relayer
"100000000000000000", // fee
"0" // refund
]
合约 verifyProof() 接收 uint[7] 作 publicSignals。Day 261 我们写合约时按此顺序消费。
9. 明日预告
Day 261: Solidity 合约实现
明天我们将:
- 用 snarkjs 生成的
Verifier.sol - 写
PrivacyPool.sol:deposit/withdraw + IncrementalBinaryTree state - 写
ComplianceOracle.sol:ASP registry + root publishing - 写
Relayer.sol:relayer 注册 + stake - Hardhat 完整测试(mock 一个 deposit→withdraw 流程)
预计代码量:~600 行 Solidity + ~300 行 TypeScript test
10. 今日复盘
学到的
- 复合电路(双 Merkle proof)的 constraint 累积接近 10000,已是 Groth16 实用上限的 1/10(大约 100K 是甜区)
- "binding 但不计算"的 public signal 必须用 quadratic constraint,否则 prover 可替换(这是 ZK 工程头号陷阱)
- 子电路独立化不只是 modularity,更是 audit 友好(PPv2 的 ASP membership 可独立审计)
卡点
- 测试用例 4(recipient 替换攻击)实际只能在端到端(合约 + Verifier.sol)层面验证,单元测试 cover 不到
- circom_tester 的 mocha 集成有时慢(~60s/test),考虑 Day 263 用 jest.config 优化
与未来工作连接
- Day 264 论文精读:可对比 Aztec Noir 写同电路,记录 LOC 与可读性差异
- 求职作品:电路代码可以贴 GitHub README,简历加"自实现 Privacy Pools v2 电路"
引用
- Day 223: Circom 入门(Num2Bits / signal / template)
- Day 224: MerkleTreeChecker 实现
- Day 225-226: Poseidon hash + Groth16 setup
- Day 227: mini Tornado withdraw.circom(本电路的直接前身)
- Day 256: Privacy Pools v2 论文 — 双 Merkle inclusion 设计来源
- Tornado Cash circuits/withdraw.circom (commit a1bbfd1, 2020-04)
- Privacy Pools v2 reference implementation (0xbow.io, 2024)
- circom 2.x manual: https://docs.circom.io
- snarkjs README: https://github.com/iden3/snarkjs
- Hermez Powers of Tau ceremony (2020): https://blog.hermez.io/hermez-cryptographic-setup/