Skip to content

Commit

Permalink
Merge branch 'main' into fix/lock-async-circuits
Browse files Browse the repository at this point in the history
  • Loading branch information
mitschabaude committed Apr 4, 2024
2 parents b7b5105 + 8dde2c3 commit f3388ee
Show file tree
Hide file tree
Showing 198 changed files with 5,422 additions and 5,105 deletions.
43 changes: 43 additions & 0 deletions .github/workflows/benchmarks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Benchmark o1js
on:
push:
branches:
- main
- berkeley
- develop
pull_request:
workflow_dispatch: {}

jobs:
benchmarks:
timeout-minutes: 30
strategy:
fail-fast: true
matrix:
node: [20]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.JS ${{ matrix.node }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- name: Build o1js and execute benchmarks on ${{ matrix.os }} and Node.JS ${{ matrix.node }}
env:
GIT_BRANCH: ${{ github.head_ref || github.ref_name }}
INFLUXDB_URL: ${{ secrets.INFLUXDB_URL }}
INFLUXDB_ORG: ${{ secrets.INFLUXDB_ORG }}
INFLUXDB_BUCKET: ${{ secrets.INFLUXDB_BUCKET }}
INFLUXDB_TOKEN: ${{ secrets.INFLUXDB_TOKEN }}
METRICS_SOURCE_ENVIRONMENT: 'o1js GitHub Actions'
METRICS_BASE_BRANCH_FOR_COMPARISON: 'main'
run: |
git submodule update --init --recursive
npm ci
npm run build
echo 'Running o1js benchmarks.'
node --enable-source-maps --stack-trace-limit=1000 src/build/run.js benchmark/runners/with-cloud-history.ts --bundle >>benchmarks.log 2>&1
cat benchmarks.log >> $GITHUB_STEP_SUMMARY
shell: bash
21 changes: 20 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,16 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Require the callback to `Mina.transaction()` to be async https://github.com/o1-labs/o1js/pull/1468
- Change `{SmartContract,ZkProgram}.analyzeMethods()` to be async https://github.com/o1-labs/o1js/pull/1450
- `Provable.runAndCheck()`, `Provable.constraintSystem()` and `{SmartContract,ZkProgram}.digest()` are also async now
- `Provable.runAndCheckSync()` added and immediately deprecated for a smoother upgrade path for tests
- **Remove deprecated APIs**
- Remove `CircuitValue`, `prop`, `arrayProp` and `matrixProp` https://github.com/o1-labs/o1js/pull/1507
- Remove `Mina.accountCreationFee()`, `Mina.BerkeleyQANet`, all APIs which accept private keys for feepayers, `Token`, `AccountUpdate.tokenSymbol`, `SmartContract.{token, setValue, setPermissions}`, "assert" methods for preconditions, `MerkleTee.calculateRootSlow()`, `Scalar.fromBigInt()`, `UInt64.lt()` and friends, deprecated static methods on `Group`, utility methods on `Circuit` like `Circuit.if()`, `Field.isZero()`, `isReady` and `shutdown()` https://github.com/o1-labs/o1js/pull/1515
- Remove `privateKey` from the accepted arguments of `SmartContract.deploy()` https://github.com/o1-labs/o1js/pull/1515
- **Efficient comparisons**. Support arbitrary bit lengths for `Field` comparisons and massively reduce their constraints https://github.com/o1-labs/o1js/pull/1523
- `Field.assertLessThan()` goes from 510 to 24 constraints, `Field.lessThan()` from 509 to 38
- Moderately improve other comparisons: `UInt64.assertLessThan()` from 27 to 14, `UInt64.lessThan()` from 27 to 15, `UInt32` similar.
- Massively improve `Field.isEven()`, add `Field.isOdd()`
- `PrivateKey.toPublicKey()` from 358 to 119 constraints thanks to `isOdd()`
- Add `Gadgets.ForeignField.assertLessThanOrEqual()` and support two variables as input to `ForeignField.assertLessThan()`
- Remove `this.sender` which unintuitively did not prove that its value was the actual sender of the transaction https://github.com/o1-labs/o1js/pull/1464 [@julio4](https://github.com/julio4)
Replaced by more explicit APIs:
- `this.sender.getUnconstrained()` which has the old behavior of `this.sender`, and returns an unconstrained value (which means that the prover can set it to any value they want)
Expand All @@ -37,13 +46,23 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
As a replacement, `UInt64.Unsafe.fromField()` was introduced
- This prevents you from accidentally creating a `UInt64` without proving that it fits in 64 bits
- Equivalent changes were made to `UInt32`
- Fixed vulnerability in `Field.to/fromBits()` outlined in [#1023](https://github.com/o1-labs/o1js/issues/1023) by imposing a limit of 254 bits https://github.com/o1-labs/o1js/pull/1461
- Remove `Field.rangeCheckHelper()` which was too low-level and easy to misuse https://github.com/o1-labs/o1js/pull/1485
- Also, rename the misleadingly named `Gadgets.isInRangeN()` to `Gadgets.isDefinitelyInRangeN()`
- Rename `Bool.Unsafe.ofField()` to `Bool.Unsafe.fromField()` https://github.com/o1-labs/o1js/pull/1485
- Replace the namespaced type exports `Gadgets.Field3` and `Gadgets.ForeignField.Sum` with `Field3` and `ForeignFieldSum`
- Unfortunately, the namespace didn't play well with auto-imports in TypeScript

### Added

- `Provable.witnessAsync()` to introduce provable values from an async callback https://github.com/o1-labs/o1js/pull/1468
- Internal benchmarking tooling to keep track of performance https://github.com/o1-labs/o1js/pull/1481
- Add `toInput` method for `Group` instance https://github.com/o1-labs/o1js/pull/1483

### Changed

- `field.assertBool()` now also returns the `Field` as a `Bool` for ergonomics https://github.com/o1-labs/o1js/pull/1523

## [0.17.0](https://github.com/o1-labs/o1js/compare/1ad7333e9e...74948acac) - 2024-03-06

### Breaking changes
Expand Down
30 changes: 22 additions & 8 deletions benchmarks/benchmark.ts → benchmark/benchmark.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
/**
* Benchmark runner
* Base benchmark harness
*/

import jStat from 'jstat';
export { BenchmarkResult, Benchmark, benchmark, printResult, pValue };
export {
Benchmark,
BenchmarkResult,
benchmark,
calculateBounds,
logResult,
pValue,
};

type BenchmarkResult = {
label: string;
Expand Down Expand Up @@ -100,11 +108,11 @@ function getStatistics(numbers: number[]) {
return { mean, variance, size: n };
}

function printResult(
function logResult(
result: BenchmarkResult,
previousResult?: BenchmarkResult
) {
console.log(result.label + `\n`);
): void {
console.log(result.label);
console.log(`time: ${resultToString(result)}`);

if (previousResult === undefined) return;
Expand All @@ -122,14 +130,13 @@ function printResult(

if (p < 0.05) {
if (result.mean < previousResult.mean) {
console.log('Performance has improved');
console.log('Performance has improved.');
} else {
console.log('Performance has regressed');
console.log('Performance has regressed.');
}
} else {
console.log('Change within noise threshold.');
}
console.log('\n');
}

function resultToString({ mean, variance }: BenchmarkResult) {
Expand All @@ -156,3 +163,10 @@ function pValue(sample1: BenchmarkResult, sample2: BenchmarkResult): number {
const pValue = 2 * (1 - jStat.studentt.cdf(Math.abs(tStatistic), df));
return pValue;
}

function calculateBounds(result: BenchmarkResult) {
const stdDev = Math.sqrt(result.variance);
const upperBound = result.mean + stdDev;
const lowerBound = result.mean - stdDev;
return { upperBound, lowerBound };
}
43 changes: 43 additions & 0 deletions benchmark/benchmarks/ecdsa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* ECDSA benchmark
*/

import { Provable } from 'o1js';
import {
Bytes32,
Ecdsa,
Secp256k1,
keccakAndEcdsa,
} from '../../src/examples/crypto/ecdsa/ecdsa.js';
import { benchmark } from '../benchmark.js';

let privateKey = Secp256k1.Scalar.random();
let publicKey = Secp256k1.generator.scale(privateKey);
let message = Bytes32.fromString("what's up");
let signature = Ecdsa.sign(message.toBytes(), privateKey.toBigInt());

const EcdsaBenchmark = benchmark(
'ecdsa',
async (tic, toc) => {
tic('build constraint system');
await keccakAndEcdsa.analyzeMethods();
toc();

tic('witness generation');
await Provable.runAndCheck(async () => {
let message_ = Provable.witness(Bytes32.provable, () => message);
let signature_ = Provable.witness(Ecdsa.provable, () => signature);
let publicKey_ = Provable.witness(Secp256k1.provable, () => publicKey);
await keccakAndEcdsa.rawMethods.verifyEcdsa(
message_,
signature_,
publicKey_
);
});
toc();
},
// two warmups to ensure full caching
{ numberOfWarmups: 2, numberOfRuns: 5 }
);

export default EcdsaBenchmark;
21 changes: 21 additions & 0 deletions benchmark/runners/simple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Simple benchmarks runner
* Exercises benchmarks and logs the results
*
* Run with
* ```
* ./run benchmark/runners/simple.ts --bundle
* ```
*/

import { logResult } from '../benchmark.js';
import EcdsaBenchmark from '../benchmarks/ecdsa.js';

// Run all benchmarks
const results = [...(await EcdsaBenchmark.run())];

// Process and log results
for (const result of results) {
logResult(result);
console.log('\n');
}
26 changes: 26 additions & 0 deletions benchmark/runners/with-cloud-history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Benchmarks runner with historical data preservation in cloud storage (InfluxDB)
*
* Run with
* ```
* ./run benchmark/runners/with-cloud-history.ts --bundle
* ```
*/

import { logResult } from '../benchmark.js';
import EcdsaBenchmark from '../benchmarks/ecdsa.js';
import {
readPreviousResultFromInfluxDb,
writeResultToInfluxDb,
} from '../utils/influxdb-utils.js';

// Run all benchmarks
const results = [...(await EcdsaBenchmark.run())];

// Process and log results
for (const result of results) {
const previousResult = await readPreviousResultFromInfluxDb(result);
logResult(result, previousResult);
writeResultToInfluxDb(result);
console.log('\n');
}
File renamed without changes.
File renamed without changes.
132 changes: 132 additions & 0 deletions benchmark/utils/influxdb-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* InfluxDB utils
*/

import { InfluxDB, Point } from '@influxdata/influxdb-client';
import os from 'node:os';
import { BenchmarkResult, calculateBounds } from '../benchmark.js';

const INFLUXDB_CLIENT_OPTIONS = {
url: process.env.INFLUXDB_URL,
token: process.env.INFLUXDB_TOKEN,
org: process.env.INFLUXDB_ORG,
bucket: process.env.INFLUXDB_BUCKET,
};
const INFLUXDB_COMMON_POINT_TAGS = {
sourceEnvironment: process.env.METRICS_SOURCE_ENVIRONMENT ?? 'local',
operatingSystem: `${os.type()} ${os.release()} ${os.arch()}`,
hardware: `${os.cpus()[0].model}, ${os.cpus().length} cores, ${(
os.totalmem() / Math.pow(1024, 3)
).toFixed(2)}Gb of RAM`,
gitBranch: process.env.GIT_BRANCH ?? 'unknown',
};
const influxDbClient = setupInfluxDbClient();

function setupInfluxDbClient(): InfluxDB | undefined {
const { url, token } = INFLUXDB_CLIENT_OPTIONS;
if (url === undefined || token === undefined) {
return undefined;
}
return new InfluxDB({ url, token });
}

export function writeResultToInfluxDb(result: BenchmarkResult): void {
const { org, bucket } = INFLUXDB_CLIENT_OPTIONS;
if (influxDbClient && org && bucket) {
console.log('Writing result to InfluxDB.');
const influxDbWriteClient = influxDbClient.getWriteApi(org, bucket, 'ms');
try {
const sampleName = result.label.split('-')[1].trim();
const { upperBound, lowerBound } = calculateBounds(result);
const point = new Point(`${result.label} - ${result.size} samples`)
.tag('benchmarkName', result.label.trim())
.tag('sampledTimes', result.size.toString())
.floatField('mean', result.mean)
.floatField('variance', result.variance)
.floatField(`${sampleName} - upperBound`, upperBound)
.floatField(`${sampleName} - lowerBound`, lowerBound)
.intField('size', result.size);
for (const [key, value] of Object.entries(INFLUXDB_COMMON_POINT_TAGS)) {
point.tag(key, value.trim());
}
influxDbWriteClient.writePoint(point);
} catch (e) {
console.error('Error writing to InfluxDB: ', e);
} finally {
influxDbWriteClient.close();
}
} else {
console.info('Skipping writing to InfluxDB: client is not configured.');
}
}

export function readPreviousResultFromInfluxDb(
result: BenchmarkResult
): Promise<BenchmarkResult | undefined> {
return new Promise((resolve) => {
const { org, bucket } = INFLUXDB_CLIENT_OPTIONS;
if (!influxDbClient || !org || !bucket) {
resolve(undefined);
return;
}
console.log('Querying InfluxDB for previous results.');
const influxDbPointTags = INFLUXDB_COMMON_POINT_TAGS;
const influxDbQueryClient = influxDbClient.getQueryApi(org);
const baseBranchForComparison =
process.env.METRICS_BASE_BRANCH_FOR_COMPARISON ?? 'main';
const fluxQuery = `
from(bucket: "${bucket}")
|> range(start: -90d)
|> filter(fn: (r) => r.benchmarkName == "${result.label}")
|> filter(fn: (r) => r.gitBranch == "${baseBranchForComparison}")
|> filter(fn: (r) => r.sampledTimes == "${result.size}")
|> filter(fn: (r) => r.sourceEnvironment == "${influxDbPointTags.sourceEnvironment}")
|> filter(fn: (r) => r.operatingSystem == "${influxDbPointTags.operatingSystem}")
|> filter(fn: (r) => r.hardware == "${influxDbPointTags.hardware}")
|> toFloat()
|> group()
|> pivot(
rowKey:["_measurement"],
columnKey: ["_field"],
valueColumn: "_value"
)
|> sort(desc: true)
|> limit(n:1)
`;
try {
let previousResult: BenchmarkResult | undefined = undefined;
influxDbQueryClient.queryRows(fluxQuery, {
next(row, tableMeta) {
const tableObject = tableMeta.toObject(row);
if (
!previousResult &&
tableObject._measurement &&
tableObject.mean &&
tableObject.variance &&
tableObject.size
) {
const measurement = tableObject._measurement;
previousResult = {
label: measurement
.substring(0, measurement.lastIndexOf('-'))
.trim(),
mean: parseFloat(tableObject.mean),
variance: parseFloat(tableObject.variance),
size: parseInt(tableObject.size, 10),
};
}
},
error(e) {
console.error('Error querying InfluxDB: ', e);
resolve(undefined);
},
complete() {
resolve(previousResult);
},
});
} catch (e) {
console.error('Error querying InfluxDB: ', e);
resolve(undefined);
}
});
}
Loading

0 comments on commit f3388ee

Please sign in to comment.