Skip to content

Instantly share code, notes, and snippets.

@kayabaNerve

kayabaNerve/.md Secret

Last active October 28, 2024 20:24
Show Gist options
  • Save kayabaNerve/c42aeae1ae9434f2678943c3b8da7898 to your computer and use it in GitHub Desktop.
Save kayabaNerve/c42aeae1ae9434f2678943c3b8da7898 to your computer and use it in GitHub Desktop.
FCMP Costs

The following script estimates the cost of FCMPs under various considerations (extending the program size to reduce the amount of commitments needed, various input counts). It also provides reference via estimating the cost of Bulletproofs+ (currently used as a range proof for the outputs).

These are not guaranteed to be 100% accurate yet should only have minor discrepancies from the actual proof sizes.

import math

print("-- FCMPs --")
for extraneous_compute in range(1, 8 + 1):
  if pow(2, math.ceil(math.log2(extraneous_compute))) != extraneous_compute:
    continue

  for i in range(1, 16 + 1):
    target_layers = 8
    proof_1_target_layers = math.floor(target_layers / 2) + (target_layers % 2)
    proof_2_target_layers = math.floor(target_layers / 2)

    proof_1_gates_for_target_layers = extraneous_compute * 256
    proof_2_gates_for_target_layers = extraneous_compute * 128

    proof_commitments = 3
    proof_1_ip_commitments = 2 * math.ceil(math.log2(i * proof_1_gates_for_target_layers))
    proof_2_ip_commitments = 2 * math.ceil(math.log2(i * proof_2_gates_for_target_layers))

    proof_1_branches = i * proof_1_target_layers
    # The root for 8 layers is on the second proof, and it's shared for all inputs
    # Accordingly, it's only committed to once
    proof_2_branches = (i * (proof_2_target_layers - 1)) + 1

    claimed_point_variables = 2 * 256
    divisor_variables = 256
    # Proof 1 has 4 claimed points, 1 append divisor, and (proof_1_target_layers - 1) claimed points
    proof_1_variables_committed_to_per_input_for_target_layers = (4 * claimed_point_variables) + divisor_variables + ((proof_1_target_layers - 1) * claimed_point_variables)
    # Proof 2 has proof_2_target_layers claimed points
    proof_2_variables_committed_to_per_input_for_target_layers = proof_2_target_layers * claimed_point_variables

    circuit_1_width = pow(2, math.ceil(math.log2(i * proof_1_gates_for_target_layers)))
    circuit_2_width = pow(2, math.ceil(math.log2(i * proof_2_gates_for_target_layers)))
    proof_1_variable_commitments = math.ceil((i * proof_1_variables_committed_to_per_input_for_target_layers) / circuit_1_width)
    proof_2_variable_commitments = math.ceil((i * proof_2_variables_committed_to_per_input_for_target_layers) / circuit_2_width)

    proof_1_vector_commitments = proof_1_branches + proof_1_variable_commitments
    proof_2_vector_commitments = proof_2_branches + proof_2_variable_commitments

    # n'
    n__1 = 2 * (math.floor(proof_1_vector_commitments / 2) + 1)
    n__2 = 2 * (math.floor(proof_2_vector_commitments / 2) + 1)
    # i == n' isn't sent
    proof_1_t_commitments = (2 * (n__1 + 1)) - 1
    proof_2_t_commitments = (2 * (n__2 + 1)) - 1

    all_proof_commitments = proof_commitments + proof_1_t_commitments + proof_1_ip_commitments + proof_1_vector_commitments + proof_commitments + proof_2_t_commitments + proof_2_ip_commitments + proof_2_vector_commitments
    all_proof_scalars = 2 * 5
    full_proof_size = (all_proof_commitments + all_proof_scalars) * 32

    print(
      "Compute Factor:", extraneous_compute,
      ", Inputs:", i,
      ", Proof 1 Base Cost:", circuit_1_width,
      ", Proof 2 Base Cost:", circuit_2_width,
      ", Commitments:", proof_commitments + proof_1_t_commitments + proof_1_ip_commitments + proof_1_vector_commitments + proof_commitments + proof_2_t_commitments + proof_2_ip_commitments + proof_2_vector_commitments,
      "(p1vcs:", proof_1_variable_commitments, ", p2vcs:", proof_2_variable_commitments, ")",
      ", Size:", full_proof_size
    )

print()
print("-- BP+ Range --")
for i in range(1, 16 + 1):
  proof_commitments = 3
  ip_commitments = 2 * math.ceil(math.log2(64 * i))
  amount_commitments = i
  commitments = proof_commitments + ip_commitments + amount_commitments
  scalars = 3
  full_proof_size = (commitments + scalars) * 32
  print("Outputs:", i, ", Base Cost:", 64 * i, ", Commitments:", commitments, ", Size:", full_proof_size)

The output is as follows.

-- FCMPs --
Compute Factor: 1 , Inputs: 1 , Proof 1 Base Cost: 256 , Proof 2 Base Cost: 128 , Commitments: 161 (p1vcs: 15 , p2vcs: 16 ) , Size: 5472
Compute Factor: 1 , Inputs: 2 , Proof 1 Base Cost: 512 , Proof 2 Base Cost: 256 , Commitments: 184 (p1vcs: 15 , p2vcs: 16 ) , Size: 6208
Compute Factor: 1 , Inputs: 3 , Proof 1 Base Cost: 1024 , Proof 2 Base Cost: 512 , Commitments: 192 (p1vcs: 12 , p2vcs: 12 ) , Size: 6464
Compute Factor: 1 , Inputs: 4 , Proof 1 Base Cost: 1024 , Proof 2 Base Cost: 512 , Commitments: 230 (p1vcs: 15 , p2vcs: 16 ) , Size: 7680
Compute Factor: 1 , Inputs: 5 , Proof 1 Base Cost: 2048 , Proof 2 Base Cost: 1024 , Commitments: 226 (p1vcs: 10 , p2vcs: 10 ) , Size: 7552
Compute Factor: 1 , Inputs: 6 , Proof 1 Base Cost: 2048 , Proof 2 Base Cost: 1024 , Commitments: 257 (p1vcs: 12 , p2vcs: 12 ) , Size: 8544
Compute Factor: 1 , Inputs: 7 , Proof 1 Base Cost: 2048 , Proof 2 Base Cost: 1024 , Commitments: 292 (p1vcs: 14 , p2vcs: 14 ) , Size: 9664
Compute Factor: 1 , Inputs: 8 , Proof 1 Base Cost: 2048 , Proof 2 Base Cost: 1024 , Commitments: 318 (p1vcs: 15 , p2vcs: 16 ) , Size: 10496
Compute Factor: 1 , Inputs: 9 , Proof 1 Base Cost: 4096 , Proof 2 Base Cost: 2048 , Commitments: 304 (p1vcs: 9 , p2vcs: 9 ) , Size: 10048
Compute Factor: 1 , Inputs: 10 , Proof 1 Base Cost: 4096 , Proof 2 Base Cost: 2048 , Commitments: 333 (p1vcs: 10 , p2vcs: 10 ) , Size: 10976
Compute Factor: 1 , Inputs: 11 , Proof 1 Base Cost: 4096 , Proof 2 Base Cost: 2048 , Commitments: 358 (p1vcs: 11 , p2vcs: 11 ) , Size: 11776
Compute Factor: 1 , Inputs: 12 , Proof 1 Base Cost: 4096 , Proof 2 Base Cost: 2048 , Commitments: 387 (p1vcs: 12 , p2vcs: 12 ) , Size: 12704
Compute Factor: 1 , Inputs: 13 , Proof 1 Base Cost: 4096 , Proof 2 Base Cost: 2048 , Commitments: 412 (p1vcs: 13 , p2vcs: 13 ) , Size: 13504
Compute Factor: 1 , Inputs: 14 , Proof 1 Base Cost: 4096 , Proof 2 Base Cost: 2048 , Commitments: 441 (p1vcs: 14 , p2vcs: 14 ) , Size: 14432
Compute Factor: 1 , Inputs: 15 , Proof 1 Base Cost: 4096 , Proof 2 Base Cost: 2048 , Commitments: 466 (p1vcs: 15 , p2vcs: 15 ) , Size: 15232
Compute Factor: 1 , Inputs: 16 , Proof 1 Base Cost: 4096 , Proof 2 Base Cost: 2048 , Commitments: 490 (p1vcs: 15 , p2vcs: 16 ) , Size: 16000
Compute Factor: 2 , Inputs: 1 , Proof 1 Base Cost: 512 , Proof 2 Base Cost: 256 , Commitments: 122 (p1vcs: 8 , p2vcs: 8 ) , Size: 4224
Compute Factor: 2 , Inputs: 2 , Proof 1 Base Cost: 1024 , Proof 2 Base Cost: 512 , Commitments: 145 (p1vcs: 8 , p2vcs: 8 ) , Size: 4960
Compute Factor: 2 , Inputs: 3 , Proof 1 Base Cost: 2048 , Proof 2 Base Cost: 1024 , Commitments: 160 (p1vcs: 6 , p2vcs: 6 ) , Size: 5440
Compute Factor: 2 , Inputs: 4 , Proof 1 Base Cost: 2048 , Proof 2 Base Cost: 1024 , Commitments: 191 (p1vcs: 8 , p2vcs: 8 ) , Size: 6432
Compute Factor: 2 , Inputs: 5 , Proof 1 Base Cost: 4096 , Proof 2 Base Cost: 2048 , Commitments: 196 (p1vcs: 5 , p2vcs: 5 ) , Size: 6592
Compute Factor: 2 , Inputs: 6 , Proof 1 Base Cost: 4096 , Proof 2 Base Cost: 2048 , Commitments: 225 (p1vcs: 6 , p2vcs: 6 ) , Size: 7520
Compute Factor: 2 , Inputs: 7 , Proof 1 Base Cost: 4096 , Proof 2 Base Cost: 2048 , Commitments: 250 (p1vcs: 7 , p2vcs: 7 ) , Size: 8320
Compute Factor: 2 , Inputs: 8 , Proof 1 Base Cost: 4096 , Proof 2 Base Cost: 2048 , Commitments: 279 (p1vcs: 8 , p2vcs: 8 ) , Size: 9248
Compute Factor: 2 , Inputs: 9 , Proof 1 Base Cost: 8192 , Proof 2 Base Cost: 4096 , Commitments: 284 (p1vcs: 5 , p2vcs: 5 ) , Size: 9408
Compute Factor: 2 , Inputs: 10 , Proof 1 Base Cost: 8192 , Proof 2 Base Cost: 4096 , Commitments: 307 (p1vcs: 5 , p2vcs: 5 ) , Size: 10144
Compute Factor: 2 , Inputs: 11 , Proof 1 Base Cost: 8192 , Proof 2 Base Cost: 4096 , Commitments: 336 (p1vcs: 6 , p2vcs: 6 ) , Size: 11072
Compute Factor: 2 , Inputs: 12 , Proof 1 Base Cost: 8192 , Proof 2 Base Cost: 4096 , Commitments: 355 (p1vcs: 6 , p2vcs: 6 ) , Size: 11680
Compute Factor: 2 , Inputs: 13 , Proof 1 Base Cost: 8192 , Proof 2 Base Cost: 4096 , Commitments: 380 (p1vcs: 7 , p2vcs: 7 ) , Size: 12480
Compute Factor: 2 , Inputs: 14 , Proof 1 Base Cost: 8192 , Proof 2 Base Cost: 4096 , Commitments: 403 (p1vcs: 7 , p2vcs: 7 ) , Size: 13216
Compute Factor: 2 , Inputs: 15 , Proof 1 Base Cost: 8192 , Proof 2 Base Cost: 4096 , Commitments: 432 (p1vcs: 8 , p2vcs: 8 ) , Size: 14144
Compute Factor: 2 , Inputs: 16 , Proof 1 Base Cost: 8192 , Proof 2 Base Cost: 4096 , Commitments: 451 (p1vcs: 8 , p2vcs: 8 ) , Size: 14752
Compute Factor: 4 , Inputs: 1 , Proof 1 Base Cost: 1024 , Proof 2 Base Cost: 512 , Commitments: 102 (p1vcs: 4 , p2vcs: 4 ) , Size: 3584
Compute Factor: 4 , Inputs: 2 , Proof 1 Base Cost: 2048 , Proof 2 Base Cost: 1024 , Commitments: 125 (p1vcs: 4 , p2vcs: 4 ) , Size: 4320
Compute Factor: 4 , Inputs: 3 , Proof 1 Base Cost: 4096 , Proof 2 Base Cost: 2048 , Commitments: 142 (p1vcs: 3 , p2vcs: 3 ) , Size: 4864
Compute Factor: 4 , Inputs: 4 , Proof 1 Base Cost: 4096 , Proof 2 Base Cost: 2048 , Commitments: 171 (p1vcs: 4 , p2vcs: 4 ) , Size: 5792
Compute Factor: 4 , Inputs: 5 , Proof 1 Base Cost: 8192 , Proof 2 Base Cost: 4096 , Commitments: 188 (p1vcs: 3 , p2vcs: 3 ) , Size: 6336
Compute Factor: 4 , Inputs: 6 , Proof 1 Base Cost: 8192 , Proof 2 Base Cost: 4096 , Commitments: 211 (p1vcs: 3 , p2vcs: 3 ) , Size: 7072
Compute Factor: 4 , Inputs: 7 , Proof 1 Base Cost: 8192 , Proof 2 Base Cost: 4096 , Commitments: 240 (p1vcs: 4 , p2vcs: 4 ) , Size: 8000
Compute Factor: 4 , Inputs: 8 , Proof 1 Base Cost: 8192 , Proof 2 Base Cost: 4096 , Commitments: 259 (p1vcs: 4 , p2vcs: 4 ) , Size: 8608
Compute Factor: 4 , Inputs: 9 , Proof 1 Base Cost: 16384 , Proof 2 Base Cost: 8192 , Commitments: 276 (p1vcs: 3 , p2vcs: 3 ) , Size: 9152
Compute Factor: 4 , Inputs: 10 , Proof 1 Base Cost: 16384 , Proof 2 Base Cost: 8192 , Commitments: 299 (p1vcs: 3 , p2vcs: 3 ) , Size: 9888
Compute Factor: 4 , Inputs: 11 , Proof 1 Base Cost: 16384 , Proof 2 Base Cost: 8192 , Commitments: 318 (p1vcs: 3 , p2vcs: 3 ) , Size: 10496
Compute Factor: 4 , Inputs: 12 , Proof 1 Base Cost: 16384 , Proof 2 Base Cost: 8192 , Commitments: 341 (p1vcs: 3 , p2vcs: 3 ) , Size: 11232
Compute Factor: 4 , Inputs: 13 , Proof 1 Base Cost: 16384 , Proof 2 Base Cost: 8192 , Commitments: 370 (p1vcs: 4 , p2vcs: 4 ) , Size: 12160
Compute Factor: 4 , Inputs: 14 , Proof 1 Base Cost: 16384 , Proof 2 Base Cost: 8192 , Commitments: 389 (p1vcs: 4 , p2vcs: 4 ) , Size: 12768
Compute Factor: 4 , Inputs: 15 , Proof 1 Base Cost: 16384 , Proof 2 Base Cost: 8192 , Commitments: 412 (p1vcs: 4 , p2vcs: 4 ) , Size: 13504
Compute Factor: 4 , Inputs: 16 , Proof 1 Base Cost: 16384 , Proof 2 Base Cost: 8192 , Commitments: 431 (p1vcs: 4 , p2vcs: 4 ) , Size: 14112
Compute Factor: 8 , Inputs: 1 , Proof 1 Base Cost: 2048 , Proof 2 Base Cost: 1024 , Commitments: 94 (p1vcs: 2 , p2vcs: 2 ) , Size: 3328
Compute Factor: 8 , Inputs: 2 , Proof 1 Base Cost: 4096 , Proof 2 Base Cost: 2048 , Commitments: 117 (p1vcs: 2 , p2vcs: 2 ) , Size: 4064
Compute Factor: 8 , Inputs: 3 , Proof 1 Base Cost: 8192 , Proof 2 Base Cost: 4096 , Commitments: 144 (p1vcs: 2 , p2vcs: 2 ) , Size: 4928
Compute Factor: 8 , Inputs: 4 , Proof 1 Base Cost: 8192 , Proof 2 Base Cost: 4096 , Commitments: 163 (p1vcs: 2 , p2vcs: 2 ) , Size: 5536
Compute Factor: 8 , Inputs: 5 , Proof 1 Base Cost: 16384 , Proof 2 Base Cost: 8192 , Commitments: 190 (p1vcs: 2 , p2vcs: 2 ) , Size: 6400
Compute Factor: 8 , Inputs: 6 , Proof 1 Base Cost: 16384 , Proof 2 Base Cost: 8192 , Commitments: 209 (p1vcs: 2 , p2vcs: 2 ) , Size: 7008
Compute Factor: 8 , Inputs: 7 , Proof 1 Base Cost: 16384 , Proof 2 Base Cost: 8192 , Commitments: 232 (p1vcs: 2 , p2vcs: 2 ) , Size: 7744
Compute Factor: 8 , Inputs: 8 , Proof 1 Base Cost: 16384 , Proof 2 Base Cost: 8192 , Commitments: 251 (p1vcs: 2 , p2vcs: 2 ) , Size: 8352
Compute Factor: 8 , Inputs: 9 , Proof 1 Base Cost: 32768 , Proof 2 Base Cost: 16384 , Commitments: 278 (p1vcs: 2 , p2vcs: 2 ) , Size: 9216
Compute Factor: 8 , Inputs: 10 , Proof 1 Base Cost: 32768 , Proof 2 Base Cost: 16384 , Commitments: 297 (p1vcs: 2 , p2vcs: 2 ) , Size: 9824
Compute Factor: 8 , Inputs: 11 , Proof 1 Base Cost: 32768 , Proof 2 Base Cost: 16384 , Commitments: 320 (p1vcs: 2 , p2vcs: 2 ) , Size: 10560
Compute Factor: 8 , Inputs: 12 , Proof 1 Base Cost: 32768 , Proof 2 Base Cost: 16384 , Commitments: 339 (p1vcs: 2 , p2vcs: 2 ) , Size: 11168
Compute Factor: 8 , Inputs: 13 , Proof 1 Base Cost: 32768 , Proof 2 Base Cost: 16384 , Commitments: 362 (p1vcs: 2 , p2vcs: 2 ) , Size: 11904
Compute Factor: 8 , Inputs: 14 , Proof 1 Base Cost: 32768 , Proof 2 Base Cost: 16384 , Commitments: 381 (p1vcs: 2 , p2vcs: 2 ) , Size: 12512
Compute Factor: 8 , Inputs: 15 , Proof 1 Base Cost: 32768 , Proof 2 Base Cost: 16384 , Commitments: 404 (p1vcs: 2 , p2vcs: 2 ) , Size: 13248
Compute Factor: 8 , Inputs: 16 , Proof 1 Base Cost: 32768 , Proof 2 Base Cost: 16384 , Commitments: 423 (p1vcs: 2 , p2vcs: 2 ) , Size: 13856

-- BP+ Range --
Outputs: 1 , Base Cost: 64 , Commitments: 16 , Size: 608
Outputs: 2 , Base Cost: 128 , Commitments: 19 , Size: 704
Outputs: 3 , Base Cost: 192 , Commitments: 22 , Size: 800
Outputs: 4 , Base Cost: 256 , Commitments: 23 , Size: 832
Outputs: 5 , Base Cost: 320 , Commitments: 26 , Size: 928
Outputs: 6 , Base Cost: 384 , Commitments: 27 , Size: 960
Outputs: 7 , Base Cost: 448 , Commitments: 28 , Size: 992
Outputs: 8 , Base Cost: 512 , Commitments: 29 , Size: 1024
Outputs: 9 , Base Cost: 576 , Commitments: 32 , Size: 1120
Outputs: 10 , Base Cost: 640 , Commitments: 33 , Size: 1152
Outputs: 11 , Base Cost: 704 , Commitments: 34 , Size: 1184
Outputs: 12 , Base Cost: 768 , Commitments: 35 , Size: 1216
Outputs: 13 , Base Cost: 832 , Commitments: 36 , Size: 1248
Outputs: 14 , Base Cost: 896 , Commitments: 37 , Size: 1280
Outputs: 15 , Base Cost: 960 , Commitments: 38 , Size: 1312
Outputs: 16 , Base Cost: 1024 , Commitments: 39 , Size: 1344

The compute factor is a time/space tradeoff. The base cost is the cost paid for verifying a batch of FCMPs (itself a pair of two proofs). The base cost paid for the entire batch is the base cost of the singular proof with the most inputs (so if all proofs are for two inputs, except a single sixteen input proof, the base cost is still 4096 + 2048). After the base cost is paid, each proof only pays its amount of commitments (so a batch of two two-input proofs and one four-input proof pays (1024 + 512) + (2 * 230) + 184). Higher compute factors do perform better when batch verifying. While they pay a higher base cost, the cost per-proof decreases.

5472 exceeds the originally estimated ~2.5kB by a notable margin. The belief for this is an under-estimation of the cost of each Pedersen Vector Commitment. While each PVC is only 32 bytes, each grows the l/r polynomials, growing the t polynomial (a result of their inner-product) by 2 coefficients. Each coefficient in the t polynomial itself has a commitment transmitted. This means each PVC actually takes 3 commitments, not 1.

p1vcs, p2vcs stands for the variable commitments. These are PVCs we use to commit to variables (which don't require any structure for their commitments). The amount of variables increase with each input, yet so does the amount of variables which can fit into a commitment. Accordingly, the amount of commitments required to commit to all the variables never actually increases.

Commitments refers to any group element transmitted during the proof however, inclusive to the variable commitments (which are PVCs), the blinded branches of the Merkle tree (which are PVCs), the t polynomial commitments, and the Bulletproof's IP argument commitments. The t polynomial only grows with PVCs, and the Bulletproof's IP argument commitments only grow 64 bytes per doubling of the program size (2 inputs, 4 inputs, 8 inputs...).

Branches can't take advantage of how with more inputs, the commitment size grows, as branches need to only contain the specific children present under the tree branch. They're mostly wasted space due to this. The recent Curve Forests proposal solves this, only having one set of branches regardless of the amount of elements proved for, by building n trees instead of just one. This increases the work a daemon must do and the work a wallet must do (either locally building the tree or downloading n times as much tree information). For a sufficiently low MAX_INPUTS, or with sufficiently high use of proofs with more inputs, this is still more computationally efficient than verifying proofs under the Curve Trees methodology. The cost of building the additional trees is less than the cost of verifying the more inefficient proofs, so long as the additional trees are used.

Curve Forests also decreases the amount of scalar multiplications which need to be done from (4 + 7) n to 4 n + 7. This would decrease the amount of variables we need to commitment to and cause less variable commitments to be used.

Beyond the Curve Forests design, we can further review Generalized Bulletproofs. The original proofs from Aaron Feickert (while at Cypher Stack) proposed doubling the amount of variables which could fit into a single PVC, without additional cost. The design failed to consider certain cross-products, and the final proofs doubled the amount of variables in a PVC while doubling the cost per-PVC (though letting us use half as many PVCs). The plan is to restore the original non-double-size PVCs (causing twice as many PVC though with half as many t commitments for them) due to their working better in our specific use-case, yet someone who can resolve the cross-products question (which Aaron didn't see a way to in the brief time they considered the problem) would reduce our amount of PVCs without increasing our amount of t commitments (achieving smaller proofs with a reduced MSM growth rate).

There was also a recent paper on extending Halo 2 to open PVCs (as necessary for the Curve {Trees, Forests} design and our scalar multiplication gadget). This was labeled as a Proof of Concept and I haven't fully sat down with it to understand how far away it is from being ready for production use, nor the literal performance which could be expected.

My immediate proposal/recommendation is set the compute factor to 4 (creating a ~3.6 kB proof with ~700 bytes growth per additional input). This will increase the base cost yet still perform well when batch verifying (such as when syncing the blockchain). This does highlight the need for optimized Helios/Selene arithmetic so that the 4x higher base cost is offset but ideally ~2x faster underlying arithmetic. Long-term, I'd propose moving to Curve Forests in a follow up hard fork. This will create an amount of branches constant to tree depth and decrease the amount of variable commitments with more inputs. This means we should expect to pay not just for one tree now yet n trees in the future (whatever MAX_INPUTS is).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment