Skip to content

Commit

Permalink
passing all tests and added README
Browse files Browse the repository at this point in the history
  • Loading branch information
okwme committed Oct 13, 2023
1 parent c0cc7b9 commit dfddf56
Show file tree
Hide file tree
Showing 31 changed files with 415 additions and 5,817 deletions.
206 changes: 205 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,208 @@
# circom-starter
# Anybody Problem

Anybody Problem is a circom project that models the movement of any number of `n` bodies using classic newtonian-like physics over any number of `s` steps. There are two versions:

## Anybody Problem NFT

The Anybody Problem NFT is a simulation of the bodies moving in space over time. Proofs are generated over `s` steps with `n` bodies and verified on-chain. This version is represented in the top level `nft.circom` circuit [here](./circuits/nft.circom).

## Anybody Problem Game

The Anybody Problem Game adds an additional `missiles` input that allows a user to fire missiles at the bodies in order to destroy them. This version is represented in the top level `stepState.circom` circuit [here](./circuits/stepState.circom). A very rough draft of the game can be played at https://okwme.github.io/anybody-problem.

## Circuits

There are a number of circuits involved in Anybody Problem. They're listed below with links and short descriptions.

> NOTICE: **Scaling Factor**
> Due to the lack of float values in circom, the values are scaled up by a factor of 10**8. This means that the values are integers and the decimal point is implied. For example, the value `1.5` would be represented as `150000000`.
> NOTICE: **Negative Values**
> Due to the nature of finite field arithmetic, negative values are represented as `p-n` where `p` is the prime number used in the finite field and `n` is the absolute value of the negative number.
>
> For example, `-1` would be represented as: `21888242871839275222246405745257275088548364400416034343698204186575808495617 - 1`
> or
> `21888242871839275222246405745257275088548364400416034343698204186575808495616`.
### [AbsoluteValueSubtraction(n)](./circuits/approxMath.circom:79)
This circuit finds the absolute value difference between two numbers.

- `n` - The maximum number of bits for each input value
- `input in[2]` - An array of length 2 for each input value
- `output out` - The difference between the two values

### [AcceptableMarginOfError(n)](./circuits/approxMath.circom:60)
This circuit is used in tandem with `approxDiv` and `approxSqrt` which are both defined in [`approxMath.circom`](./circuits/approxMath.circom). When finding the approximate solution to division or the approximate square root of a value, there is an acceptable margin of error to be expected. This circuit ensures the value is within that range.

- `n` - The maximum number of bits for each input value
- `input expected` - The expected value
- `input actual` - The actual value
- `input marginOfError` - The margin of error
- `output out` - The output value is `0` when the difference is outside the margin of error and `1` when within the margin of error.

### [CalculateForce()](./circuits/calculateForce.circom)
This circuit calculates the gravitational force between two bodies based on their mass and position in space.
- `input in_bodies[2][5]` - An array of length 2 for each body. Each body has 5 inputs: `position_x`, `position_y`, `vector_x`, `vector_y` and `radius/mass`. These are all scaled up by a factor of 10**8.
- `output out_forces` - An array of length 2 for each force, `force_x` and `force_y`

### [DetectCollision(totalBodies)](./circuits/detectCollision.circom)
This circuit detects if a body and a missile are colliding. It does this by calculating the distance between the two and comparing it to the sum of their radii. If a collision is detected, the radius/mass of the body and the missile are returned as 0.
- `totalBodies` - The total number of bodies in the simulation
- `input bodies[totalBodies][5]` - An array of length `totalBodies` for each body. Each body has 5 inputs: `position_x`, `position_y`, `vector_x`, `vector_y` and `radius/mass`
- `input missile[5]` - An array of length 5 for the missile. Each missile has 5 inputs: `position_x`, `position_y`, `vector_x`, `vector_y` and `radius/mass`. These are all scaled up by a factor of 10**8.
- `output out_bodies[totalBodies][5]` - An array of length `totalBodies` for each body. Each body has 5 outputs: `position_x`, `position_y`, `vector_x`, `vector_y` and `radius/mass`. These are all scaled up by a factor of 10**8.
- `output out_missile[5]` - An array of length 5 for the missile. Each missile has 5 outputs: `position_x`, `position_y`, `vector_x`, `vector_y` and `radius/mass`. These are all scaled up by a factor of 10**8.

### [ForceAccumulator(totalBodies)](./circuits/forceAccumulator.circom)
This circuit calculates the total force on each body based on the gravitational force between each pair of bodies. It limits the vector by a maximum value and updates the position of each body based on its accumulated force. If the new position is outside the boundary of the simulation, it is updated to be on the opposite side as if the area was a torus.
- `totalBodies` - The total number of bodies in the simulation
- `input in_bodies[totalBodies][5]` - An array of length `totalBodies` for each body. Each body has 5 inputs: `position_x`, `position_y`, `vector_x`, `vector_y` and `radius/mass`. These are all scaled up by a factor of 10**8.
- `output out_bodies[totalBodies][5]` - An array of length `totalBodies` for each body. Each body has 5 outputs: `position_x`, `position_y`, `vector_x`, `vector_y` and `radius/mass`. These are all scaled up by a factor of 10**8.

### [GetDistance(n)](./circuits/getDistance.circom)
This circuit calculates the distance between two coordinate points using `approxSqrt()` and checking the result is within an acceptable margin of error using `AcceptableMarginOfError()`.
- `n` - The maximum number of bits for each input value and expected output value.
- `input x1` - The x coordinate of the first point
- `input y1` - The y coordinate of the first point
- `input x2` - The x coordinate of the second point
- `input y2` - The y coordinate of the second point
- `output distance` - The distance between the two points

### [Limiter(n)](./circuits/limiter.circom)
This circuit limits the value of an input to a maximum value. It also accepts an alternative value that is returned if the input value is greater than the maximum.
- `n` - The maximum number of bits for each input value and expected output value.
- `input in` - The input value
- `input limit` - The maximum value
- `input rather` - The alternative value
- `output out` - The output value

### [LowerLimiter(n)](./circuits/limiter.circom:23)
This circuit limits the value of an input to a minimum value. It also accepts an alternative value that is returned if the input value is less than the minimum.
- `n` - The maximum number of bits for each input value and expected output value.
- `input in` - The input value
- `input limit` - The minimum value
- `input rather` - The alternative value
- `output out` - The output value

### [nft(totalBodies, steps)](./circuits/nft.circom)
This circuit is the top level circuit for the NFT version of Anybody Problem. It takes in the initial state of the bodies and the number of steps to simulate and outputs the resulting bodies.
- `totalBodies` - The total number of bodies in the simulation
- `steps` - The total number of steps to simulate
- `input bodies[totalBodies][5]` - An array of length `totalBodies` for each body. Each body has 5 inputs: `position_x`, `position_y`, `vector_x`, `vector_y` and `radius/mass`. These are all scaled up by a factor of 10**8.
- `output out_bodies[totalBodies][5]` - An array of length `totalBodies` for each body. Each body has 5 outputs: `position_x`, `position_y`, `vector_x`, `vector_y` and `radius/mass`. These are all scaled up by a factor of 10**8.

### [StepState(totalBodies, steps)](./circuits/stepState.circom)
This is the top level circuit for the game version of Anybody Problem. It takes in the initial state of the bodies, the number of steps to simulate and the missiles to fire and outputs the resulting bodies.
- `totalBodies` - The total number of bodies in the simulation.
- `steps` - The total number of steps to simulate.
- `input bodies[totalBodies][5]` - An array of length `totalBodies` for each body. Each body has 5 inputs: `position_x`, `position_y`, `vector_x`, `vector_y` and `radius/mass`. These are all scaled up by a factor of 10**8.
- `input missiles[steps + 1][5]` - An array of length `steps + 1` for each missile. Each missile has 5 inputs: `position_x`, `position_y`, `vector_x`, `vector_y` and `radius/mass`. These are all scaled up by a factor of 10**8.
- `output out_bodies[totalBodies][5]` - An array of length `totalBodies` for each body. Each body has 5 outputs: `position_x`, `position_y`, `vector_x`, `vector_y` and `radius/mass`. These are all scaled up by a factor of 10**8.


## Tests

To run rudimentary tests on the various circuits use the following command:

```bash
yarn test
```

You should see something like:
```bash
absoluteValueSubtraction circuit
✔ produces a witness with valid constraints (97ms)
✔ has expected witness values
✔ has the correct output (53ms)

acceptableMarginOfError circuit
✔ produces a witness with valid constraints (145ms)
✔ has expected witness values
✔ has the correct output

calculateForceMain circuit
✔ produces a witness with valid constraints (1076ms)
✔ has expected witness values (43ms)
✔ has the correct output

detectCollisionMain circuit
✔ produces a witness with valid constraints (855ms)
✔ has expected witness values
✔ has the correct output

forceAccumulatorMain circuit
✔ produces a witness with valid constraints (232ms)
✔ has expected witness values (53ms)
✔ has the correct output

getDistanceMain circuit
✔ produces a witness with valid constraints (898ms)
✔ has expected witness values (90ms)
✔ has the correct output (41ms)

limiterMain circuit
✔ produces a witness with valid constraints (95ms)
✔ has expected witness values
✔ has the correct output (57ms)

lowerLimiterMain circuit
✔ produces a witness with valid constraints (975ms)
✔ has expected witness values
✔ has the correct output (48ms)

nft circuit
✔ produces a witness with valid constraints (1043ms)
✔ has expected witness values (324ms)
✔ has the correct output (185ms)

stepStateMain circuit
✔ produces a witness with valid constraints (1520ms)
✔ has expected witness values (485ms)
✔ has the correct output (238ms)


30 passing (28s)

✨ Done in 30.33s.
```

## Performance

Currently the project is targeting [powersOfTau28_hez_final_20.ptau](https://github.com/iden3/snarkjs/blob/master/README.md#7-prepare-phase-2) which has a limit of 1MM constraints. Below is a table of the number of constraints used by each circuit.

| Circuit | Non-Linear Constraints |
|---------|-------------|
| absoluteValueSubtraction(252) | 257 |
| acceptableMarginOfError(60) | 126 |
| calculateForce() | 842 |
| detectCollision(3) | 1,548 |
| forceAccumulator(3) | 4,518 |
| getDistance(252) | 1,026 |
| limiter(252) | 254 |
| lowerLimiter(252) | 254 |
| nft(2, 1) | 2,170 |
| nft(2, 10) | 21,700 |
| nft(2, 100) | 217,000 |
| nft(3, 1) | 4,518 |
| nft(3, 10) | 45,180 |
| nft(3, 100) | 451,800 |
| nft(4, 1) | 7708 |
| nft(4, 10) | 77,080 |
| nft(4, 100) | 770,800 |
| nft(5, 1) | 11,740 |
| nft(5, 10) | 117,400 |
| nft(5, 100) | 1,174,000 |
| stepState(2, 1) | 3,293 |
| stepState(2, 10) | 32,930 |
| stepState(2, 100) | 329,300 |
| stepState(3, 1) | 6,157 |
| stepState(3, 10) | 61,570 |
| stepState(3, 100) | 615,700 |
| stepState(4, 1) | 9,863 |
| stepState(4, 10) | 98,630 |
| stepState(4, 100) | 986,300 |

# built using circom-starter

A basic circom project using [Hardhat](https://github.com/nomiclabs/hardhat) and [hardhat-circom](https://github.com/projectsophon/hardhat-circom). This combines the multiple steps of the [Circom](https://github.com/iden3/circom) and [SnarkJS](https://github.com/iden3/snarkjs) workflow into your [Hardhat](https://hardhat.org) workflow.

Expand Down
2 changes: 1 addition & 1 deletion circuits/acceptableMarginOfError.circom
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ pragma circom 2.1.6;

include "approxMath.circom";

component main { public [ val1, val2, marginOfError ] } = AcceptableMarginOfError(60);
component main { public [ expected, actual, marginOfError ] } = AcceptableMarginOfError(60);
4 changes: 2 additions & 2 deletions circuits/acceptableMarginOfError.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"val1": "992745205956",
"val2": "992744209590",
"expected": "992745205956",
"actual": "992744209590",
"marginOfError": "1992732"
}
19 changes: 5 additions & 14 deletions circuits/approxMath.circom
Original file line number Diff line number Diff line change
Expand Up @@ -57,26 +57,17 @@ function approxDiv(dividend, divisor) {
}


// var approxQuot = approxDiv(realNumerator, realDenominator);
// approxNumerator = approxQuot * realDenominator;
// diff = realNumerator - approxNumerator;
// diff <= (realDenominator / 2) + 1;// +1 for odd number denominators
// diff - 1 <== realDenominator / 2
// (2 * diff) - 2 <== realDenominator
// realDenominator <== (2 * diff) - 2


template AcceptableMarginOfError (n) {
signal input val1;
signal input val2;
signal input expected;
signal input actual;
signal input marginOfError;
signal output out;


// The following is to ensure diff = Abs(val2 - val1)
// The following is to ensure diff = Abs(actual - expected)
component absoluteValueSubtraction = AbsoluteValueSubtraction(n);
absoluteValueSubtraction.in[0] <== val1;
absoluteValueSubtraction.in[1] <== val2;
absoluteValueSubtraction.in[0] <== expected;
absoluteValueSubtraction.in[1] <== actual;

// Now ensure that the diff is less than the margin of error
component lessThan2 = LessEqThan(n);
Expand Down
12 changes: 6 additions & 6 deletions circuits/calculateForce.circom
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ template CalculateForce() {
// log("distanceSquared", distanceSquared);
// bits should be maximum of the vectorLimiter which would be (10 * 10 ** 8) * (10 * 10 ** 8) which is under 60 bits
component acceptableMarginOfError = AcceptableMarginOfError(60); // TODO: test the limits of this.
acceptableMarginOfError.val1 <== distanceSquared;
acceptableMarginOfError.val2 <== distance ** 2;
acceptableMarginOfError.expected <== distance ** 2;
acceptableMarginOfError.actual <== distanceSquared;
// margin of error should be midpoint between squares
acceptableMarginOfError.marginOfError <== distance * 2; // TODO: confrim if (distance * 2) +1 is needed
acceptableMarginOfError.out === 1;
Expand Down Expand Up @@ -144,8 +144,8 @@ template CalculateForce() {
// NOTE: the following constraints the approxDiv to ensure it's within the acceptable error of margin
signal approxNumerator1 <== forceXunsigned * forceDenom;
component acceptableErrorOfMarginDiv1 = AcceptableMarginOfError(divisionBits); // TODO: test the limits of this.
acceptableErrorOfMarginDiv1.val1 <== forceXnum;
acceptableErrorOfMarginDiv1.val2 <== approxNumerator1;
acceptableErrorOfMarginDiv1.expected <== forceXnum;
acceptableErrorOfMarginDiv1.actual <== approxNumerator1;
acceptableErrorOfMarginDiv1.marginOfError <== forceDenom; // TODO: actually could be further reduced to (realDenom / 2) + 1 but then we're using division again
acceptableErrorOfMarginDiv1.out === 1;

Expand All @@ -167,8 +167,8 @@ template CalculateForce() {
// NOTE: the following constraints the approxDiv to ensure it's within the acceptable error of margin
signal approxNumerator2 <== forceYunsigned * forceDenom;
component acceptableErrorOfMarginDiv2 = AcceptableMarginOfError(divisionBits); // TODO: test the limits of this.
acceptableErrorOfMarginDiv2.val1 <== forceYnum;
acceptableErrorOfMarginDiv2.val2 <== approxNumerator2;
acceptableErrorOfMarginDiv2.expected <== forceYnum;
acceptableErrorOfMarginDiv2.actual <== approxNumerator2;
acceptableErrorOfMarginDiv2.marginOfError <== forceDenom; // TODO: actually could be further reduced to (realDenom / 2) + 1 but then we're using division again
acceptableErrorOfMarginDiv2.out === 1;

Expand Down
2 changes: 1 addition & 1 deletion circuits/calculateMissile.circom
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ template CalculateMissile() {

// TODO: confirm the max vector of missiles (may change frequently)
var maxVector = 1000000000; // using 10^8 decimals
log("maxVector", maxVector);
// log("maxVector", maxVector);
// NOTE: windowWidth appears in forceAccumulator as well and needs to match
var windowWidth = 100000000000; // using 10^8 decimals

Expand Down
2 changes: 1 addition & 1 deletion circuits/detectCollision.circom
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ template DetectCollision(totalBodies) {
// log("bodies[0][0]", bodies[0][0]);
// log("bodies[0][1]", bodies[0][1]);
// log("-133000000 ", -133000000);
log("-1 (p-1) ", -1);
// log("-1 (p-1) ", -1);
// log("bodies[0][2]", bodies[0][2]);
// log("bodies[0][3]", bodies[0][3]);
// log("bodies[0][4]", bodies[0][4]);
Expand Down
23 changes: 0 additions & 23 deletions circuits/division.circom

This file was deleted.

6 changes: 0 additions & 6 deletions circuits/division.json

This file was deleted.

8 changes: 4 additions & 4 deletions circuits/forceAccumulator.circom
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ template ForceAccumulator(totalBodies) {
force_x[ii] <== calculateForceComponent[ii].out_forces[0];
force_y[ii] <== calculateForceComponent[ii].out_forces[1];
// accumulate the value of the force on body i and body j
log("j", j, "force_x[ii]", force_x[ii]);
// log("j", j, "force_x[ii]", force_x[ii]);
accumulated_body_forces[i][0] = accumulated_body_forces[i][0] + force_x[ii];
accumulated_body_forces[i][1] = accumulated_body_forces[i][1] + force_y[ii];
accumulated_body_forces[j][0] = accumulated_body_forces[j][0] - force_x[ii];
Expand All @@ -75,8 +75,8 @@ template ForceAccumulator(totalBodies) {
// calculate the new vector for body i
new_vector_x[i] <== bodies[i][2] + accumulated_body_forces[i][0];
new_vector_y[i] <== bodies[i][3] + accumulated_body_forces[i][1];
log(i, "accumulated_body_forces[i][0]", accumulated_body_forces[i][0]);
log(i, "new_vector_x[i][2]", new_vector_x[i]);
// log(i, "accumulated_body_forces[i][0]", accumulated_body_forces[i][0]);
// log(i, "new_vector_x[i][2]", new_vector_x[i]);

// limit the magnitude of the vector
vectorLimiterX[i] = Limiter(252); // TODO: confirm bits limit
Expand Down Expand Up @@ -113,7 +113,7 @@ template ForceAccumulator(totalBodies) {
positionLowerLimiterY[i].limit <== maxVector;
positionLowerLimiterY[i].rather <== windowWidth + maxVector;
out_bodies[i][1] <== positionLowerLimiterY[i].out - maxVector;
log(i, "out_bodies[i][2]", out_bodies[i][2]);
// log(i, "out_bodies[i][2]", out_bodies[i][2]);
}
// log("out_bodies[0][0]", out_bodies[0][0]);
// log("out_bodies[0][1]", out_bodies[0][1]);
Expand Down
4 changes: 2 additions & 2 deletions circuits/getDistance.circom
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ template GetDistance(n) {
distance <-- approxSqrt(distanceSquared);
// bits should be maximum of the vectorLimiter which would be (10 * 10 ** 8) * (10 * 10 ** 8) which is under 60 bits
component acceptableMarginOfError = AcceptableMarginOfError(n); // TODO: test the limits of this.
acceptableMarginOfError.val1 <== distanceSquared;
acceptableMarginOfError.val2 <== distance ** 2;
acceptableMarginOfError.expected <== distanceSquared;
acceptableMarginOfError.actual <== distance ** 2;
// margin of error should be midpoint between squares
acceptableMarginOfError.marginOfError <== distance * 2; // TODO: confrim if (distance * 2) +1 is needed
acceptableMarginOfError.out === 1;
Expand Down
Loading

0 comments on commit dfddf56

Please sign in to comment.