Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,11 @@
- name: Execute tests
env:
TEST_TYPE: ${{ matrix.test_type }}
O1JS_EXPERIMENTAL_MONTGOMERY_MSM: ${{ matrix.test_type == 'Performance Regression' && '1' || '0' }}
O1JS_EXPERIMENTAL_MONTGOMERY_COMMIT_MSM: ${{ matrix.test_type == 'Performance Regression' && '1' || '0' }}
O1JS_EXPERIMENTAL_MONTGOMERY_PROVER_MSM: ${{ matrix.test_type == 'Performance Regression' && '1' || '0' }}
O1JS_EXPERIMENTAL_MONTGOMERY_PROVER_BATCH_MSM: ${{ matrix.test_type == 'Performance Regression' && '1' || '0' }}
O1JS_MONTGOMERY_MSM_TRACE: ${{ matrix.test_type == 'Performance Regression' && '1' || '0' }}
run: sh run-ci-tests.sh
- name: Add to job summary
if: always()
Expand Down Expand Up @@ -315,7 +320,7 @@

echo "Running tests from index $start_index to $end_index"

shopt -s globstar

Check warning on line 323 in .github/workflows/checks.yml

View workflow job for this annotation

GitHub Actions / Lint-Format-and-Typo-Check

Unknown word (shopt)
all_tests=(./dist/node/**/*.unit-test.js)
test_files=()
for test in "${all_tests[@]}"; do
Expand Down Expand Up @@ -444,6 +449,11 @@
env:
TEST_TYPE: ${{ matrix.test_type }}
O1JS_BACKEND: native
O1JS_EXPERIMENTAL_MONTGOMERY_MSM: ${{ matrix.test_type == 'Performance Regression' && '1' || '0' }}
O1JS_EXPERIMENTAL_MONTGOMERY_COMMIT_MSM: ${{ matrix.test_type == 'Performance Regression' && '1' || '0' }}
O1JS_EXPERIMENTAL_MONTGOMERY_PROVER_MSM: ${{ matrix.test_type == 'Performance Regression' && '1' || '0' }}
O1JS_EXPERIMENTAL_MONTGOMERY_PROVER_BATCH_MSM: ${{ matrix.test_type == 'Performance Regression' && '1' || '0' }}
O1JS_MONTGOMERY_MSM_TRACE: ${{ matrix.test_type == 'Performance Regression' && '1' || '0' }}
run: sh run-ci-tests.sh
- name: Add to job summary
if: always()
Expand Down Expand Up @@ -502,7 +512,7 @@
exit 1
fi

shopt -s globstar

Check warning on line 515 in .github/workflows/checks.yml

View workflow job for this annotation

GitHub Actions / Lint-Format-and-Typo-Check

Unknown word (shopt)
test_files=(./dist/node/**/*.unit-test.js)

start_index=$(( (TOTAL_TESTS * (CHUNK - 1) / CHUNKS) ))
Expand Down
12 changes: 10 additions & 2 deletions benchmark/benchmarks/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ const TxnBenchmarks = benchmark(
await transaction.send();
toc();
},
// two warmups to ensure full caching
{ numberOfWarmups: 2, numberOfRuns: 5 }
// two warmups to ensure full caching by default
{
numberOfWarmups: readBenchmarkCount('O1JS_TXN_BENCH_WARMUPS', 2),
numberOfRuns: readBenchmarkCount('O1JS_TXN_BENCH_RUNS', 5),
}
);

function readBenchmarkCount(env: string, fallback: number) {
let value = Number(process.env[env] ?? fallback);
return Number.isInteger(value) && value >= 0 ? value : fallback;
}
183 changes: 183 additions & 0 deletions benchmark/runners/srs-msm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/**
* SRS MSM benchmark for the experimental montgomery backend.
*
* Run with:
* ```
* ./run benchmark/runners/srs-msm.ts
* ```
*
* Optional knobs:
* - O1JS_MSM_BENCH_SIZES=14,16,18
* - O1JS_MSM_BENCH_THREADS=4
* - O1JS_MSM_BENCH_WHOLE_DOMAIN=1
*/

import type { MlArray } from '../../src/lib/ml/base.js';
import type { OrInfinity } from '../../src/bindings/crypto/bindings/curve.js';
import type { PolyComm } from '../../src/bindings/crypto/bindings/kimchi-types.js';

type FieldName = 'fp' | 'fq';

const fields: FieldName[] = ['fp', 'fq'];
const logs = parseLogSizes(process.env.O1JS_MSM_BENCH_SIZES ?? '14,16,18');
const threads = Number(process.env.O1JS_MSM_BENCH_THREADS ?? 0);
const includeWholeDomain = process.env.O1JS_MSM_BENCH_WHOLE_DOMAIN === '1';
const oldMontgomeryFlag = process.env.O1JS_EXPERIMENTAL_MONTGOMERY_MSM;
const bindings = await importInternal<any>('../../bindings.js', '../../src/bindings.js');
const { getRustConversion } = await importInternal<any>(
'../../bindings/crypto/bindings.js',
'../../src/bindings/crypto/bindings.js'
);
const { srs: createSrsBindings } = await importInternal<any>(
'../../bindings/crypto/bindings/srs.js',
'../../src/bindings/crypto/bindings/srs.js'
);
const { computeMontgomeryLagrangeCommitment, initializeMontgomeryMsm } =
await importInternal<any>(
'../../bindings/crypto/bindings/montgomery-msm.js',
'../../src/bindings/crypto/bindings/montgomery-msm.js'
);

await bindings.initializeBindings();
let wasm = bindings.wasm;
let conversion = getRustConversion(wasm);
let srsBindings = createSrsBindings(wasm, conversion as any);

let initialized = await measure('montgomery cold start', () =>
initializeMontgomeryMsm({ force: true, threads })
);
if (initialized.value !== true) {
throw Error(
'montgomery backend did not initialize. Install optional dependency `montgomery` and run on Node 24+ or a supported browser runtime.'
);
}

await bindings.withThreadPool(async () => {
for (let field of fields) {
for (let logSize of logs) {
let domainSize = 1 << logSize;
let srs = (
await measure(`${field} 2^${logSize} kimchi srs create`, () =>
Promise.resolve(srsBindings[field].create(domainSize))
)
).value;

let indices = [0, domainSize >> 1, domainSize - 1];
let getSrsPoints = () =>
(conversion as any)[field].pointsFromRust(
wasm[`caml_${field}_srs_get`](srs)
) as MlArray<OrInfinity>;

for (let index of indices) {
process.env.O1JS_EXPERIMENTAL_MONTGOMERY_MSM = '';
let kimchi = await measure(`${field} 2^${logSize} kimchi lagrange[${index}]`, () =>
Promise.resolve(srsBindings[field].lagrangeCommitment(srs, domainSize, index))
);

let first = await measure(`${field} 2^${logSize} montgomery first[${index}]`, () =>
computeMontgomeryLagrangeCommitment({
field,
srs,
domainSize,
index,
getSrsPoints,
options: { force: true, threads },
})
);
assertSamePolyComm(
first.value as PolyComm | undefined,
kimchi.value,
`${field} 2^${logSize} first[${index}]`
);

let warm = await measure(`${field} 2^${logSize} montgomery warm[${index}]`, () =>
computeMontgomeryLagrangeCommitment({
field,
srs,
domainSize,
index,
getSrsPoints,
options: { force: true, threads },
})
);
assertSamePolyComm(
warm.value as PolyComm | undefined,
kimchi.value,
`${field} 2^${logSize} warm[${index}]`
);
}

if (includeWholeDomain) {
process.env.O1JS_EXPERIMENTAL_MONTGOMERY_MSM = '';
await measure(`${field} 2^${logSize} kimchi whole-domain`, () =>
Promise.resolve(srsBindings[field].lagrangeCommitmentsWholeDomain(srs, domainSize))
);
}
}
}
});

if (oldMontgomeryFlag === undefined) {
delete process.env.O1JS_EXPERIMENTAL_MONTGOMERY_MSM;
} else {
process.env.O1JS_EXPERIMENTAL_MONTGOMERY_MSM = oldMontgomeryFlag;
}

async function measure<T>(label: string, run: () => Promise<T>) {
let start = performance.now();
let value = await run();
let ms = performance.now() - start;
console.log(`${label}: ${ms.toFixed(3)}ms`);
return { value, ms };
}

function parseLogSizes(input: string) {
return input
.split(',')
.map((x) => Number(x.trim()))
.filter((x) => Number.isInteger(x) && x > 0);
}

async function importInternal<T>(distSpecifier: string, sourceSpecifier: string): Promise<T> {
let specifiers = [distSpecifier, sourceSpecifier];
for (let specifier of specifiers) {
try {
return (await import(specifier)) as T;
} catch (error) {
if (!isModuleNotFound(error)) throw error;
}
}
throw Error(`could not import ${distSpecifier} or ${sourceSpecifier}`);
}

function isModuleNotFound(error: unknown) {
if (!(error instanceof Error)) return false;
return (
'code' in error &&
((error as { code?: unknown }).code === 'ERR_MODULE_NOT_FOUND' ||
(error as { code?: unknown }).code === 'MODULE_NOT_FOUND')
);
}

function assertSamePolyComm(actual: PolyComm | undefined, expected: PolyComm, label: string) {
if (actual === undefined) throw Error(`${label}: montgomery returned no commitment`);
if (!polyCommEquals(actual, expected)) throw Error(`${label}: commitment mismatch`);
}

function polyCommEquals(a: PolyComm, b: PolyComm) {
let aPoints = a[1];
let bPoints = b[1];
if (aPoints.length !== bPoints.length) return false;
for (let i = 1; i < aPoints.length; i++) {
let aPoint = aPoints[i];
let bPoint = bPoints[i];
if (aPoint === 0 || bPoint === 0) {
if (aPoint !== bPoint) return false;
continue;
}
if (aPoint[1][1][1] !== bPoint[1][1][1] || aPoint[1][2][1] !== bPoint[1][2][1]) {
return false;
}
}
return true;
}
85 changes: 85 additions & 0 deletions benchmark/runners/transaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Focused transaction benchmark runner for end-to-end compile/prove timing.
*
* Run with:
* ```
* O1JS_TXN_BENCH_RUNS=1 ./run benchmark/runners/transaction.ts
* ```
*/

import {
AccountUpdate,
Field,
Mina,
PrivateKey,
SmartContract,
State,
initializeBindings,
method,
state,
} from 'o1js';

class SimpleZkapp extends SmartContract {
@state(Field) x = State<Field>();

init() {
super.init();
this.x.set(Field(1));
}

@method async noOp() {}
}

await initializeBindings();

let runs = readBenchmarkCount('O1JS_TXN_BENCH_RUNS', 1);
let Local = await Mina.LocalBlockchain({
proofsEnabled: true,
enforceTransactionLimits: true,
});
Mina.setActiveInstance(Local);

let transactionFee = 100_000_000;
let [feePayer] = Local.testAccounts;

let { verificationKey } = await measure('simple zkapp compile', () => SimpleZkapp.compile());

for (let run = 0; run < runs; run++) {
let zkappPrivateKey = PrivateKey.random();
let zkapp = new SimpleZkapp(zkappPrivateKey.toPublicKey());

let deployTransaction = await measure('simple zkapp deploy transaction construction', () =>
Mina.transaction({ sender: feePayer, fee: transactionFee }, async () => {
AccountUpdate.fundNewAccount(feePayer);
await zkapp.deploy({ verificationKey });
})
);
await measure('simple zkapp deploy transaction signing', async () => {
deployTransaction.sign([feePayer.key, zkappPrivateKey]);
});
await measure('simple zkapp deploy transaction local sending', () => deployTransaction.send());

let callTransaction = await measure('simple zkapp call transaction construction', () =>
Mina.transaction({ sender: feePayer, fee: transactionFee }, async () => {
await zkapp.noOp();
})
);
await measure('simple zkapp call transaction proving', () => callTransaction.prove());
await measure('simple zkapp call transaction signing', async () => {
callTransaction.sign([feePayer.key]);
});
await measure('simple zkapp call transaction local sending', () => callTransaction.send());
}

async function measure<T>(label: string, run: () => Promise<T>) {
let start = performance.now();
let value = await run();
let ms = performance.now() - start;
console.log(`${label}: ${ms.toFixed(3)}ms`);
return value;
}

function readBenchmarkCount(env: string, fallback: number) {
let value = Number(process.env[env] ?? fallback);
return Number.isInteger(value) && value >= 0 ? value : fallback;
}
2 changes: 1 addition & 1 deletion npmDepsHash
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sha256-pKzMJ78FJBWm6SxOoRmzBVbsUZRUSHgX/oXOZO0Aqtw=
sha256-1fIW5ylFDZFs3PLIZpTYxuWH/meXL95umHri/jqSK0c=
Loading
Loading