Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hashed provable type #1377

Merged
merged 10 commits into from
Feb 1, 2024
Merged
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
11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,20 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
_Security_ in case of vulnerabilities.
-->

## [Unreleased](https://github.com/o1-labs/o1js/compare/be748e42e...HEAD)
## [Unreleased](https://github.com/o1-labs/o1js/compare/e5d1e0f...HEAD)

### Breaking changes

- Reduce number of constraints of ECDSA verification by 5%, which breaks deployed contracts using ECDSA https://github.com/o1-labs/o1js/pull/1376

### Added

- Provable type `Packed<T>` to pack small field elements into fewer field elements https://github.com/o1-labs/o1js/pull/1376
- Provable type `Hashed<T>` to represent provable types by their hash https://github.com/o1-labs/o1js/pull/1377
- This also exposes `Poseidon.hashPacked()` to efficiently hash an arbitrary type

## [0.15.4](https://github.com/o1-labs/o1js/compare/be748e42e...e5d1e0f)

### Changed

- Improve performance of Wasm Poseidon hashing by a factor of 13x https://github.com/o1-labs/o1js/pull/1378
Expand All @@ -33,7 +41,6 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Configurable `networkId` when declaring a Mina instance. https://github.com/o1-labs/o1js/pull/1387
- Defaults to `"testnet"`, the other option is `"mainnet"`
- The `networkId` parameter influences the algorithm used for signatures, and ensures that testnet transactions can't be replayed on mainnet
- Provable type `Packed<T>` to pack small field elements into fewer field elements https://github.com/o1-labs/o1js/pull/1376

## [0.15.3](https://github.com/o1-labs/o1js/compare/1ad7333e9e...be748e42e)

Expand Down
2 changes: 1 addition & 1 deletion src/bindings
1 change: 0 additions & 1 deletion src/examples/crypto/ecdsa/ecdsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
createEcdsa,
createForeignCurve,
Bool,
Keccak,
Bytes,
} from 'o1js';

Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export { Provable } from './lib/provable.js';
export { Circuit, Keypair, public_, circuitMain } from './lib/circuit.js';
export { UInt32, UInt64, Int64, Sign, UInt8 } from './lib/int.js';
export { Bytes } from './lib/provable-types/provable-types.js';
export { Packed } from './lib/provable-types/packed.js';
export { Packed, Hashed } from './lib/provable-types/packed.js';
export { Gadgets } from './lib/gadgets/gadgets.js';
export { Types } from './bindings/mina-transaction/types.js';

Expand Down
2 changes: 1 addition & 1 deletion src/lib/gadgets/ecdsa.unit-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ let msgHash =
);

const ia = initialAggregator(Secp256k1);
const config = { G: { windowSize: 4 }, P: { windowSize: 3 }, ia };
const config = { G: { windowSize: 4 }, P: { windowSize: 4 }, ia };

let program = ZkProgram({
name: 'ecdsa',
Expand Down
20 changes: 10 additions & 10 deletions src/lib/gadgets/elliptic-curve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { provable } from '../circuit_value.js';
import { assertPositiveInteger } from '../../bindings/crypto/non-negative.js';
import { arrayGet, assertBoolean } from './basic.js';
import { sliceField3 } from './bit-slices.js';
import { Packed } from '../provable-types/packed.js';
import { Hashed } from '../provable-types/packed.js';

// external API
export { EllipticCurve, Point, Ecdsa };
Expand Down Expand Up @@ -228,7 +228,7 @@ function verifyEcdsa(
G?: { windowSize: number; multiples?: Point[] };
P?: { windowSize: number; multiples?: Point[] };
ia?: point;
} = { G: { windowSize: 4 }, P: { windowSize: 3 } }
} = { G: { windowSize: 4 }, P: { windowSize: 4 } }
) {
// constant case
if (
Expand Down Expand Up @@ -414,12 +414,12 @@ function multiScalarMul(
sliceField3(s, { maxBits, chunkSize: windowSizes[i] })
);

// pack points to make array access more efficient
// a Point is 6 x 88-bit field elements, which are packed into 3 field elements
const PackedPoint = Packed.create(Point.provable);
// hash points to make array access more efficient
// a Point is 6 field elements, the hash is just 1 field element
const HashedPoint = Hashed.create(Point.provable);

let packedTables = tables.map((table) =>
table.map((point) => PackedPoint.pack(point))
let hashedTables = tables.map((table) =>
table.map((point) => HashedPoint.hash(point))
);

ia ??= initialAggregator(Curve);
Expand All @@ -436,10 +436,10 @@ function multiScalarMul(
windowSize === 1
? points[j]
: arrayGetGeneric(
PackedPoint.provable,
packedTables[j],
HashedPoint.provable,
hashedTables[j],
sj
).unpack();
).unhash();

// ec addition
let added = add(sum, sjP, Curve);
Expand Down
22 changes: 22 additions & 0 deletions src/lib/hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export { Poseidon, TokenSymbol };

// internal API
export {
ProvableHashable,
HashInput,
HashHelpers,
emptyHashWithPrefix,
Expand All @@ -24,6 +25,9 @@ export {
hashConstant,
};

type Hashable<T> = { toInput: (x: T) => HashInput; empty: () => T };
type ProvableHashable<T> = Provable<T> & Hashable<T>;

class Sponge {
#sponge: unknown;

Expand Down Expand Up @@ -96,6 +100,24 @@ const Poseidon = {
return { x, y: { x0, x1 } };
},

/**
* Hashes a provable type efficiently.
*
* ```ts
* let skHash = Poseidon.hashPacked(PrivateKey, secretKey);
* ```
*
* Note: Instead of just doing `Poseidon.hash(value.toFields())`, this
* uses the `toInput()` method on the provable type to pack the input into as few
* field elements as possible. This saves constraints because packing has a much
* lower per-field element cost than hashing.
*/
hashPacked<T>(type: Hashable<T>, value: T) {
let input = type.toInput(value);
let packed = packToFields(input);
return Poseidon.hash(packed);
},

initialState(): [Field, Field, Field] {
return [Field(0), Field(0), Field(0)];
},
Expand Down
123 changes: 118 additions & 5 deletions src/lib/provable-types/packed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import {
} from '../circuit_value.js';
import { Field } from '../field.js';
import { assert } from '../gadgets/common.js';
import { Poseidon, packToFields } from '../hash.js';
import { Poseidon, ProvableHashable, packToFields } from '../hash.js';
import { Provable } from '../provable.js';
import { fields } from './fields.js';
import { fields, modifiedField } from './fields.js';

export { Packed };
export { Packed, Hashed };

/**
* Packed<T> is a "packed" representation of any type T.
Expand Down Expand Up @@ -63,6 +63,10 @@ class Packed<T> {
packed: fields(packedSize),
value: Unconstrained.provable,
}) as ProvableHashable<Packed<T>>;

static empty(): Packed<T> {
return Packed_.pack(type.empty());
}
};
}

Expand Down Expand Up @@ -120,8 +124,6 @@ class Packed<T> {
}
}

type ProvableHashable<T> = Provable<T> & { toInput: (x: T) => HashInput };

function countFields(input: HashInput) {
let n = input.fields?.length ?? 0;
let pendingBits = 0;
Expand All @@ -137,3 +139,114 @@ function countFields(input: HashInput) {

return n;
}

/**
* Hashed<T> represents a type T by its hash.
*
* Since a hash is only a single field element, this can be more efficient in provable code
* where the number of constraints depends on the number of field elements per value.
*
* For example, `Provable.if(bool, x, y)` takes O(n) constraints, where n is the number of field
* elements in x and y. With Hashed, this is reduced to O(1).
*
* The downside is that you will pay the overhead of hashing your values, so it helps to experiment
* in which parts of your code a hashed representation is beneficial.
*
* Usage:
*
* ```ts
* // define a hashed type from a type
* let HashedType = Hashed.create(MyType);
*
* // hash a value
* let hashed = HashedType.hash(value);
*
* // ... operations on hashes, more efficient than on plain values ...
*
* // unhash to get the original value
* let value = hashed.unhash();
* ```
*/
class Hashed<T> {
hash: Field;
value: Unconstrained<T>;

/**
* Create a hashed representation of `type`. You can then use `HashedType.hash(x)` to wrap a value in a `Hashed`.
*/
static create<T>(
type: ProvableHashable<T>,
hash?: (t: T) => Field
): typeof Hashed<T> {
let _hash = hash ?? ((t: T) => Poseidon.hashPacked(type, t));

let dummyHash = _hash(type.empty());

return class Hashed_ extends Hashed<T> {
static _innerProvable = type;
static _provable = provableFromClass(Hashed_, {
hash: modifiedField({ empty: () => dummyHash }),
value: Unconstrained.provable,
}) as ProvableHashable<Hashed<T>>;

static _hash = _hash satisfies (t: T) => Field;

static empty(): Hashed<T> {
return new this(dummyHash, Unconstrained.from(type.empty()));
}
};
}

constructor(hash: Field, value: Unconstrained<T>) {
this.hash = hash;
this.value = value;
}

static _hash(_: any): Field {
assert(false, 'Hashed not initialized');
}

/**
* Wrap a value, and represent it by its hash in provable code.
*/
static hash<T>(value: T): Hashed<T> {
let hash = this._hash(value);
return new this(hash, Unconstrained.from(value));
}

/**
* Unwrap a value from its hashed variant.
*/
unhash(): T {
let value = Provable.witness(this.Constructor.innerProvable, () =>
this.value.get()
);

// prove that the value hashes to the hash
let hash = this.Constructor._hash(value);
this.hash.assertEquals(hash);

return value;
}

toFields(): Field[] {
return [this.hash];
}

// dynamic subclassing infra
static _provable: ProvableHashable<Hashed<any>> | undefined;
static _innerProvable: ProvableHashable<any> | undefined;

get Constructor(): typeof Hashed {
return this.constructor as typeof Hashed;
}

static get provable(): ProvableHashable<Hashed<any>> {
assert(this._provable !== undefined, 'Hashed not initialized');
return this._provable;
}
static get innerProvable(): ProvableHashable<any> {
assert(this._innerProvable !== undefined, 'Hashed not initialized');
return this._innerProvable;
}
}
Loading
Loading