On the fourth day of the audit, the prover's p99 wall-clock time fell from 14.2 seconds to 11.6 seconds. One substitution produced it. A range check that had been expressed as sixteen boolean constraints became a single 16-bit lookup argument. The verifier's on-chain gas cost rose 3.1% in the same commit. Nobody on the review call flagged the second number.
That asymmetry is the subject of this essay. Proof generation time is the number teams publish. Proof verification cost is the number that decides whether a rollup survives fee compression. The two move in opposite directions more often than the industry's benchmark tables admit. And the trade-off is locked at the circuit level, months before any public dashboard exists.
I spent two weeks inside that constraint system in the first quarter of 2026. I reduced proof generation time by 18%. I also found four gadgets where the constraint set was satisfiable by witnesses the developer never intended to accept. The first result went into the team's launch post. The second result is the reason I am writing this one.
Code does not lie, only the documentation does.
The Four-Stage Pipeline and Where the Cost Lives
A zero-knowledge rollup is not one system. It is four systems stacked, and each one has a different cost curve.
Stage one is execution. The sequencer runs transactions against state, produces a trace, and emits a witness. Stage two is arithmetization. The trace is converted into a constraint system — a set of polynomial equations that must evaluate to zero at every row of the execution table. Stage three is proving. A prover commits to polynomials, runs a polynomial commitment scheme, and produces a proof object. Stage four is verification. A verifier contract on the base layer checks the proof and updates a state root.
Most public discussion collapses these into "the rollup." That collapse hides the only thing that matters for cost engineering: each stage has its own bottleneck, and optimizing one usually taxes another.
Witness generation is memory-bound. It is pointer chasing across a table with irregular access patterns. Adding more cores helps until the memory bus saturates, then it stops helping entirely.
Arithmetization is a design decision made once and paid for forever. Every column you add to the execution table is a column the prover must commit to. Every custom gate you define is a constraint you no longer need to express as a boolean circuit, but it is also a constraint the auditor must reason about as a unit.

Proving is compute-bound in the multi-scalar multiplication and the number-theoretic transform. This is where GPUs earn their place, and where the 18% number came from.
Verification is gas-bound. It is a fixed number of elliptic curve operations, a fixed number of field element reads from calldata, and a pairing check. It is small. It is also the only stage that recurs on every single batch, forever, at mainnet prices.
That last point deserves emphasis. A prover runs once per batch on hardware you amortize. A verifier runs once per batch on Ethereum, at whatever the base fee happens to be. A 3.1% increase in verification gas is not a rounding error. It is a permanent annuity paid to the base layer.
If it cannot be verified, it cannot be trusted. That principle applies to the verifier itself, not just to the statement it checks.
Why Constraints Are the Unit of Account
The constraint count is the closest thing this industry has to a cost of goods sold.
A constraint is one row of the arithmetization. If your circuit has N constraints, your prover performs a number-theoretic transform over a domain of size roughly N, and an MSM over roughly N group elements. Both scale worse than linearly once you exceed cache. Doubling the constraint count does not double proving time. It multiplies it by something between 2.2 and 2.9 depending on memory hierarchy and commitment scheme.
This is why the industry converged on lookup arguments. A lookup lets you prove "this value is a member of this table" without decomposing the value into bits and constraining each bit. A 16-bit range check goes from sixteen constraints to one. A full 32-bit decomposition goes from thirty-two to two. On paper, that is a 16x reduction in the range-check budget.
The paper is correct. The paper is also incomplete. A lookup argument does not eliminate work. It relocates it. The prover must now commit to a permutation polynomial that encodes the multiset equality between the query column and the table column. That permutation is logarithmic in the table size, but it is not free, and it adds a second commitment the verifier must open.
The result is a transfer of cost from the prover's arithmetic to the verifier's calldata and pairing budget. In a market where block space is expensive and prover hardware is not, that transfer can be a net loss even when it produces a headline speedup.
The Sideways Tape and the Fee Floor
I track L2 fee floors the way equity analysts track gross margins. Over the trailing ninety days, the median cost to post a batch on the base layer has moved in a narrow band while the median cost to verify a proof has drifted upward across the sector. My own dashboard, which samples the top dozen rollups by batch frequency, shows verification gas rising while batch posting cost stays flat.
The mechanism is not mysterious. Teams are competing on throughput. Throughput is a function of proving time. Proving time is a function of constraint count and commitment scheme. The cheapest way to cut constraint count is to add custom gates and lookups. Each of those adds verifier work.
In a rising-fee environment, nobody notices. In a sideways tape, the annuity becomes visible. A rollup that pays 3% more to verify, at four thousand batches a day, is paying for a speedup its users cannot observe.
This is the structural signal I look for in consolidation markets. Not which project has the best headline TPS. Which project has the flattest cost curve across both stages. Chop is for positioning, and positioning requires reading the second derivative.
The Constraint Budget, Before and After
The project I audited had a circuit of 2^21 rows. That is a comfortable size — large enough to hold a meaningful batch, small enough to prove on a single consumer GPU without sharding. The team's constraint breakdown, as I measured it before any changes:
| Constraint category | Count (millions) | Share of total | Notes | |---|---|---|---| | Range checks (bit decomposition) | 4.61 | 22.0% | 8/16/32-bit, hand-rolled | | Signature verification (ECDSA recovery) | 3.98 | 19.0% | Non-native field arithmetic | | Keccak permutation | 3.14 | 15.0% | State transition hashing | | Merkle path verification | 2.72 | 13.0% | Depth-32, binary | | Arithmetic / ALU | 2.51 | 12.0% | Add, mul, div, comparison | | Memory consistency | 2.09 | 10.0% | Permutation argument | | Lookup bookkeeping | 1.05 | 5.0% | Permutation commitments | | Control flow / selectors | 0.84 | 4.0% | Branchless dispatch | | Total | 20.94 | 100% | 2^21 domain, ~99.8% utilized |
Two observations. First, range checks and signature verification together account for 41% of the circuit. Both are candidates for lookup-based compression. Second, the circuit was already at 99.8% domain utilization. There was no headroom. Any optimization that added rows would have forced a domain doubling to 2^22, which would have cost more than the optimization saved.
That constraint — no domain headroom — is the single most common failure mode I see in circuit reviews. Teams optimize a gadget in isolation, the gadget adds bookkeeping rows, the domain doubles, and the prover gets slower. The measurement was done on a subcircuit. The production circuit regressed.
I measured at the full circuit level. Every change below was validated against the complete 2^21 domain.
The Domain Doubling Trap
The trap is worth isolating because it inverts the sign of an optimization.
Suppose you compress a gadget from 400,000 constraints to 90,000. That looks like a 310,000-row win. Now suppose the compression requires a new lookup table with a permutation commitment that adds 40,000 bookkeeping rows and forces one additional column in the execution trace. Your net row change is still negative. Your column count is up by one.
Columns are the multiplier. The prover's commitment work scales with rows times columns for the permutation arguments. A 15% row reduction paired with a 3% column increase can produce a net slowdown.
| Scenario | Rows | Columns | Domain | Prover time | Verdict | |---|---|---|---|---|---| | Baseline | 20.94M | 214 | 2^21 | 14.2s | — | | Gadget-only view | 20.63M | 214 | 2^21 | 13.9s (est.) | Looks like a win | | Full-circuit view | 20.63M | 217 | 2^22 | 31.4s | Regression | | My change (range + allocator) | 17.88M | 216 | 2^21 | 10.0s | Real win |
The lesson generalizes. Never accept a benchmark that does not state the domain size and the column count. A prover time without those two numbers is not a measurement. It is a claim.
Where the Wall Clock Actually Goes
Before touching a constraint, I instrumented the pipeline. The team's internal benchmark reported "proof time: 14.2s." That number was measuring the prover binary's wall clock, which excludes witness generation and excludes aggregation.
| Pipeline stage | Time (s) | Share | Bound by | |---|---|---|---| | Witness generation | 6.9 | 40.6% | Memory bandwidth | | Polynomial commitment (MSM) | 3.4 | 20.0% | GPU throughput | | Number-theoretic transform | 2.1 | 12.4% | GPU throughput | | Lookup permutation build | 1.6 | 9.4% | Memory bandwidth | | Aggregation / recursion | 1.9 | 11.2% | Sequential dependency | | Proof serialization | 0.7 | 4.1% | CPU single-thread | | Quotient polynomial computation | 0.4 | 2.4% | GPU throughput | | Total | 17.0 | 100% | |
Two things fell out of this table immediately.
First, the headline 14.2 seconds was 83.5% of the real cost. The missing 2.8 seconds lived in witness generation and serialization, both outside the prover binary's timer. This is not fraud. It is a scoping decision that became a marketing number.
Second, 50% of the pipeline was memory-bound, not compute-bound. Adding GPUs to the MSM and NTT stages would have produced almost nothing. The team had already provisioned for compute. They had not provisioned for bandwidth.
I spent three days on witness generation before touching a single constraint. The trace writer was allocating a new buffer per column instead of using a preallocated arena. Fixing that, plus reordering the memory consistency table to improve locality, cut witness generation from 6.9 seconds to 4.4 seconds. No constraint changed. No verifier gas changed. Proof time dropped 14.7%.
That is the least glamorous optimization in the entire discipline, and it is consistently the highest-yield one.
Security is a process, not a feature. So is performance. Neither is a one-time commit.
The Lookup Trade, Quantified
With witness generation fixed, I turned to the range checks. The 8-bit and 16-bit decompositions were hand-rolled as boolean constraint chains. Both are textbook lookup candidates.
The change: replace the 8-bit and 16-bit range checks with two lookup tables, sized 256 and 65,536 respectively.
| Metric | Before | After | Delta | |---|---|---|---| | Range check constraints | 4.61M | 0.29M | -93.7% | | Lookup bookkeeping constraints | 1.05M | 2.31M | +120% | | Net constraint change | — | — | -3.06M | | Domain size | 2^21 | 2^21 | unchanged | | Witness generation | 4.4s | 4.1s | -6.8% | | MSM time | 3.4s | 2.9s | -14.7% | | NTT time | 2.1s | 1.8s | -14.3% | | Lookup permutation build | 1.6s | 2.4s | +50% | | Verifier gas | 412,000 | 425,000 | +3.1% | | Total prover time | 12.2s | 10.0s | -18.0% |
The net constraint reduction was 3.06 million rows, a 14.6% reduction. The domain stayed at 2^21 because the circuit had been at 99.8% utilization and the freed rows simply reduced utilization to 85.2%. That headroom is now available for the next feature. It is also, quietly, the most valuable output of the exercise.
The verifier gas increase came from two sources. The lookup argument requires the verifier to recompute the table commitment, which for a 65,536-entry table is a nontrivial MSM. And the permutation argument adds one more opening per proof.
Here is the number the team did not put in the launch post. At 4,200 batches per day, a 13,000 gas increase per verification is 54.6 million gas per day. At a 12 gwei base fee and a nominal ETH price, that is a real annual line item. The 18% prover speedup is amortized over hardware the team already owns. The 3.1% verification increase is paid forever.
I recommended they keep the change anyway. The domain headroom is worth more than the gas, because it defers a 2^22 migration that would have cost roughly 2.4x in prover time. But the recommendation came with a condition: publish both numbers, or neither.
Aggregation Is the Hidden Tax
Aggregation deserves its own section because it is where the cost model breaks.
A recursive proof composes N inner proofs into one outer proof. The economic argument is straightforward: verify once instead of N times. The argument is correct on the verifier side and wrong on the prover side, and the crossover point is rarely computed.
The project aggregated in groups of 8. Inner proof time was 10.0 seconds after my changes. Aggregation added 1.9 seconds. So the effective cost per transaction batch was 11.9 seconds, and the verifier paid one 425,000-gas check instead of eight.
| Aggregation depth | Prover time per inner batch | Verifier gas per inner batch | Effective cost index | |---|---|---|---| | 1 (none) | 10.0s | 425,000 | 1.00 | | 4 | 10.6s | 106,250 | 0.71 | | 8 | 11.9s | 53,125 | 0.66 | | 16 | 14.8s | 26,563 | 0.72 | | 32 | 21.3s | 13,281 | 0.94 |
The cost index is my own construct: prover seconds plus verifier gas normalized to a common unit using a fixed hardware-to-gas exchange rate. It is not a universal metric. It is a decision tool. The shape is what matters.
Depth 8 is the minimum. Depth 16 is worse than depth 8 on this circuit because the outer circuit's fixed overhead grows faster than the per-inner saving. Depth 32 is actively harmful. The team had been running depth 8 by intuition. The intuition was correct. They had never verified it.
The general lesson: aggregation depth is a circuit parameter with an optimum, and the optimum is specific to your constraint count, your commitment scheme, and the prevailing gas price. It is not a constant you inherit from a template.
The Verifier Gas Curve Across Schemes
I benchmarked four verification paths on the same statement to isolate scheme-level cost from circuit-level cost. All numbers are for a 2^21-row circuit with an equivalent constraint count.
| Scheme | Proof size | Verifier gas | Trusted setup | Notes | |---|---|---|---|---| | Groth16 | 192 bytes | 218,000 | Per-circuit | Cheapest verify, rigid circuit | | PLONK (KZG) | 768 bytes | 412,000 | Universal, updatable | The project's baseline | | PLONK (hybrid FRI) | 42 KB | 1,340,000 | Transparent | Post-quantum posture, high calldata | | STARK (no recursion) | 180 KB | 4,900,000 | Transparent | Not viable for per-batch verification |
The gap between Groth16 and PLONK-KZG is roughly 1.9x in verification gas. That gap is the price of a universal setup, which is the price of not running a per-circuit ceremony every time you change the circuit.
This is the trade nobody states plainly. Groth16 is cheaper to verify and more expensive to evolve. Every circuit change requires a new ceremony, and a ceremony is a trust assumption with a social cost. PLONK costs 1.9x at verification and lets you ship a circuit change in a week.
The project chose PLONK-KZG. Given that they shipped three circuit revisions during my engagement, that was the correct call. But the call has a price, and the price appears on the base layer every fifteen seconds.
A note on the trusted setup, because it is the most under-audited component in the entire stack. The universal setup's toxic waste is a global secret. If it leaked, an attacker could forge proofs for any circuit using that setup. The ceremony had 4,100 contributors, which is a healthy number. The contribution transcript was verified against the published hash chain. What I could not verify — and what no auditor can verify from outside — is whether the entropy sources were actually independent. The transcript proves the contributions were applied in order. It does not prove they were random.
That is a trust assumption sitting underneath a system marketed as trustless. It is a small one, correctly bounded, and it should be stated out loud.
Memory Bandwidth, Not Arithmetic
I want to dwell on this because it inverts the standard optimization playbook.
Fifty percent of the pipeline was memory-bound. The trace is a wide table — hundreds of columns, millions of rows — and the access pattern during witness generation is irregular. The memory consistency argument requires the prover to build a permutation over the address space, which means random access into a multi-gigabyte structure.
The team had provisioned eight accelerators for proving. The MSM stage was using them at 61% utilization. The witness stage was running on the host CPU and saturating a single memory channel.
We moved witness generation onto a NUMA-aware configuration with interleaved allocation across channels. Throughput improved 36%. Cost: zero. No new hardware, no new constraints, no verifier gas change.
| Configuration | Witness gen time | Memory utilization | Notes | |---|---|---|---| | Single-channel, host CPU | 6.9s | 94% (one channel) | Baseline | | NUMA-interleaved, 4 channels | 4.4s | 71% (four channels) | Zero hardware cost | | + arena allocator | 4.1s | 68% | Removed per-column malloc | | + reordered consistency table | 3.6s | 61% | Locality improvement |
The final witness time was 3.6 seconds, down from 6.9 — a 47.8% reduction in the stage that was never benchmarked. In the launch post, this showed up as part of the 18% total. It was, by my measurement, the largest single contributor.
The general principle: before you buy an accelerator, profile your memory bus. Before you rewrite a gadget, profile your allocator. The unglamorous layers are where the returns are, and they are unglamorous precisely because nobody markets them.
The Four Under-Constrained Gadgets
Now the part that matters more than the speedup.
While instrumenting the circuit, I built a satisfiability harness. The harness takes the constraint system, strips the witness-generation logic, and asks the solver to find any assignment that satisfies all constraints. If the solver finds a satisfying assignment that the witness generator would never produce, the circuit is under-constrained.
This is the standard test. It is also the test almost nobody runs, because it requires decoupling the circuit from the generator and because a well-optimized circuit produces thousands of satisfying assignments that are all benign.
The harness found four that were not benign.
| Gadget | Issue | Severity | Exploitable? | |---|---|---|---| | 8-bit range check (lookup) | Table held 256 entries but the query column was not constrained to the table's multiplicity bound | High | Yes — permits out-of-range values in packed fields | | Signature recovery | Non-native field arithmetic did not constrain the high limb of the scalar after reduction | Critical | Yes — permits malleable signatures | | Merkle path | Leaf-vs-internal selector was a free variable at row 0 | Medium | No — caught by a downstream equality check | | Memory consistency | Permutation argument did not bind the timestamp column to the address column | High | Conditional — requires sequencer collusion |
The signature recovery finding is the one that kept me on the call past midnight.
The circuit verifies ECDSA recovery over secp256k1 inside a field of different characteristic. That requires non-native field arithmetic: represent a 256-bit scalar as limbs in the native field, do the arithmetic, then reduce. The reduction step constrains the low 128 bits. The high 128 bits were constrained by a range check that, after my lookup optimization, had become satisfiable by values in the range [2^128, 2^129).
That is a one-bit gap. A one-bit gap in a signature scalar is enough to produce two distinct valid signatures for the same message and the same key. In an account-abstraction context, that is a replay surface.
I found it because I changed the range check. The optimization I made for speed exposed a constraint that had been load-bearing. This is not a coincidence. It is the standard failure mode: optimizations remove constraints that were doing security work nobody documented.
Code does not lie, only the documentation does. The documentation said the range check enforced 128 bits. The code enforced 127, after my change. Before my change, it enforced 128, but only because the boolean decomposition happened to be correct — not because anyone had designed it that way.
I reverted the lookup for that specific gadget, kept it for the others, and added the harness to the project's CI. The 18% figure survived. It would have been 19.4% without the revert.
What "Verified" Actually Means
Here is the contrarian claim, and it is the reason I am publishing this rather than filing it privately.
A ZK-rollup's proof says one thing: there exists a witness satisfying this constraint system. It does not say the constraint system describes the intended semantics. It does not say the witness corresponds to a valid execution of the EVM. It does not say the sequencer included the transactions it claimed to include.
The gap between "the proof verifies" and "the rollup is correct" is the entire security surface, and it is not covered by the proof.
Three layers sit in that gap.
Layer one is the circuit's fidelity to the execution semantics. Every opcode the rollup claims to support must have a constraint gadget that matches the EVM specification. Divergence here is a consensus bug that produces valid proofs of invalid state. I have seen this in three audits. It is always found by differential testing against a reference implementation, never by reading the circuit.
Layer two is the witness generator. The prover's witness generator is trusted code. If it produces a witness that satisfies the constraints but does not correspond to the trace, the proof is valid and the state root is wrong. The constraint system cannot catch this. Only a second, independent implementation can.
Layer three is the sequencer. The proof covers state transition, not transaction inclusion. A sequencer can censor, reorder, or withhold, and every proof it produces will verify. This is the well-known centralization critique, and it is correct, and it is orthogonal to everything the proving system does.
My audit covered layer one partially and layer two not at all. The project had no independent witness implementation. I flagged it. The flag was accepted as a roadmap item.
The industry's benchmark tables measure the prover. The industry's risk is concentrated in the three layers around it.
If it cannot be verified, it cannot be trusted. But "verified" is a narrower word than the marketing implies.
A Note on AI in the Verification Path
I have spent the last year benchmarking AI-driven oracle nodes against deterministic price feeds. The finding that has held across every configuration: AI-generated data introduces measurable variance that deterministic feeds do not. In my 2025 study of twenty hybrid nodes under high-frequency conditions, price feed variance ran 12% above the deterministic baseline.
I raise it here because the same pattern is appearing in proving infrastructure. Teams are beginning to use learned models to predict constraint-system bottlenecks, auto-tune aggregation depth, and select commitment parameters. Every one of those is a non-deterministic decision inside a deterministic pipeline.
An auto-tuner that picks aggregation depth 16 instead of 8 does not produce an invalid proof. It produces a valid proof at 9% higher cost. That is tolerable. An auto-tuner that relaxes a constraint to hit a latency target produces an invalid circuit, and the proof still verifies.
The rule I apply: AI may propose, but a deterministic check must dispose. Every parameter an ML system selects must be re-derived by an auditable function before it reaches the circuit. If the re-derivation disagrees, the circuit wins.
This is not Luddism. It is the same discipline I apply to any optimizer. The compiler is allowed to be clever. The verifier is not.
Regulatory Compliance
I include this section in every deep-dive because the technical decisions above have legal consequences that the teams making them rarely map.
The trusted setup is the sharpest example. A universal setup with 4,100 contributors is a multi-party computation with a documented transcript. Under most frameworks, that transcript is the evidentiary artifact. If a regulator or a counterparty asks whether the system's integrity assumptions are documented, the ceremony transcript is the answer. Projects that run a ceremony and then lose the transcript have a technical asset and no compliance asset.
The second item is attestation. A rollup that publishes a state root every fifteen seconds is, functionally, publishing a continuous attestation. Custodians and auditors increasingly treat those roots as the control point. The scriptPubKey mismatch I found during the 2024 custody review taught me the same lesson at a smaller scale: a one-byte encoding discrepancy in an output script is a delivery failure, and a delivery failure is a legal event, not a bug ticket. The same logic applies to a state root that diverges from the intended semantics. The proof verifies. The asset does not arrive.
The third item is the witness generator. If the generator is a single trusted implementation maintained by the operating entity, then the entity is a control point in the sense that custody rules use the term. That has implications for how the rollup is classified in a custody arrangement. I have watched two institutional reviews stall on exactly this question.
None of this is a reason to slow down. It is a reason to document the constraint system's security assumptions alongside its performance numbers. A circuit that is fast and undocumented is an operational risk with a benchmark attached.
The Contrarian Angle: Proof Time Is a Vanity Metric
The sector benchmarks proving throughput. I think that is the wrong number, and I think the reason it persists is that it is the only number that is easy to measure.
Prover time is measured on hardware the team controls. Verification gas is measured on a base layer the team does not control, at a price that moves. Teams publish the first because it is stable and flattering. They omit the second because it is volatile and unflattering.
But the second is the one that compounds. A 3% verification increase is permanent and applies to every batch forever. A 20% prover speedup applies to hardware you replace every eighteen months.
Consider two rollups. Rollup A proves in 8 seconds and verifies at 500,000 gas. Rollup B proves in 14 seconds and verifies at 380,000 gas. At current base fees, B is cheaper to operate by a wide margin, and B is the one that will survive a fee spike. Every benchmark table in the industry would rank A above B.
There is a second blind spot, and it is worse. The benchmark tables do not measure under-constraint. A circuit that is 15% faster because it dropped a range check will top every leaderboard and pass every performance review. It will fail a satisfiability harness in four places.
I found the one-bit gap in the signature gadget because I changed the range check for speed. If I had only been measuring, I would have measured a correct-looking number and moved on.
The third blind spot is aggregation depth, which I covered above. The optimum is circuit-specific and gas-price-dependent. Most teams pick a constant and never revisit it. In a sideways market with flat fees, an unexamined aggregation constant is a slow leak.
There is a fourth blind spot, and it is the one I expect to matter most over the next cycle. Circuits are becoming libraries. Teams import gadgets from shared repositories the way Solidity developers import OpenZeppelin. The reentrancy failures I catalogued in the EtherDelta contracts in 2018 were not novel bugs. They were well-known patterns applied in the wrong context. The same migration is happening in circuit code right now, and the shared libraries do not yet have the equivalent of a formal invariant checklist. A gadget that is correct in a 32-bit context is not correct in a 64-bit context. A range check that is sound for one table size is unsound for another. Import without re-derivation is how the next class of consensus bugs arrives.
Takeaway
The 18% figure is real. The team hit mainnet two weeks early. I am not disputing either.
What I want on the record is the arithmetic underneath. Three million constraints removed. Domain headroom recovered from 0.2% to 14.8%. Verification gas up 3.1%, permanently. Four under-constrained gadgets found, one of them critical, one of them created by the optimization that produced the headline number.
The forward-looking question is not whether ZK-rollups can prove faster. They can, and they will, and the curve is steep. The question is whether the industry will adopt a disclosure norm that puts prover time, verifier gas, constraint count, and under-constraint harness results in the same table.
My forecast: the first rollup to publish all four will be treated as an outlier, then as a standard, and the transition will take about eighteen months. In the interim, the differentiator in a flat tape is not throughput. It is whether anyone on the team can tell you the verifier gas number without opening a dashboard.
Ask the next team you evaluate for four numbers. If they have three, you have learned something about the fourth.
Security is a process, not a feature. So is disclosure.