Skip to content

Commit

Permalink
Merge pull request #1377 from o1-labs/feature/hashed-provable
Browse files Browse the repository at this point in the history
Hashed provable type
  • Loading branch information
mitschabaude committed Feb 1, 2024
2 parents 9c55fba + ef5cc97 commit d99fceb
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 31 deletions.
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

0 comments on commit d99fceb

Please sign in to comment.