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

Simplify hashToGroup #1546

Merged
merged 5 commits into from
Apr 16, 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Fixes soundness of ECDSA; slightly increases its constraints from ~28k to 29k
- Breaks circuits that used EC addition, like ECDSA
- `Mina.LocalBlockchain` no longer supports the network kind configuration https://github.com/o1-labs/o1js/pull/1581
- `Poseidon.hashToGroup()` now returns a `Group` directly, and constrains it to be deterministic https://github.com/o1-labs/o1js/pull/1546
- Added `Poseidon.Unsafe.hashToGroup()` as a more efficient, non-deterministic version for advanced use cases

### Changes

Expand Down
2 changes: 1 addition & 1 deletion src/bindings
17 changes: 3 additions & 14 deletions src/lib/provable/crypto/nullifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,7 @@ class Nullifier extends Struct({
let pk_fields = Group.toFields(publicKey);

// x and y of hash(msg, pk), it doesn't return a Group because y is split into x0 and x1, both two roots of a field element
let {
x,
y: { x0 },
} = Poseidon.hashToGroup([...message, ...pk_fields]);

// check to prevent the prover from using the second square root and forging a non-unique nullifier
x0.isEven().assertTrue();

let h_m_pk = Group.fromFields([x, x0]);
let h_m_pk = Poseidon.hashToGroup([...message, ...pk_fields]);

// pk^c
let pk_c = this.publicKey.scale(c);
Expand All @@ -83,8 +75,7 @@ class Nullifier extends Struct({
Poseidon.hash([
...Group.toFields(G),
...pk_fields,
x,
x0,
...Group.toFields(h_m_pk),
...Group.toFields(nullifier),
...Group.toFields(g_r),
...Group.toFields(h_m_pk_s_div_nullifier_s),
Expand Down Expand Up @@ -191,9 +182,7 @@ class Nullifier extends Struct({

const r = Scalar.random();

const gm = Hash([...message, ...Group.toFields(pk)]);

const h_m_pk = Group({ x: gm.x, y: gm.y.x0 });
const h_m_pk = Hash([...message, ...Group.toFields(pk)]);

const nullifier = h_m_pk.scale(sk.toBigInt());
const h_m_pk_r = h_m_pk.scale(r.toBigInt());
Expand Down
61 changes: 34 additions & 27 deletions src/lib/provable/crypto/poseidon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Poseidon as PoseidonBigint } from '../../../bindings/crypto/poseidon.js
import { assert } from '../../util/errors.js';
import { rangeCheckN } from '../gadgets/range-check.js';
import { TupleN } from '../../util/types.js';
import { Group } from '../group.js';

// external API
export { Poseidon, TokenSymbol };
Expand Down Expand Up @@ -78,38 +79,44 @@ const Poseidon = {
return [Field(0), Field(0), Field(0)];
},

hashToGroup(input: Field[]) {
if (isConstant(input)) {
let result = PoseidonBigint.hashToGroup(toBigints(input));
assert(result !== undefined, 'hashToGroup works on all inputs');
let { x, y } = result;
return {
x: Field(x),
y: { x0: Field(y.x0), x1: Field(y.x1) },
};
}

// y = sqrt(y^2)
let [, xv, yv] = Snarky.poseidon.hashToGroup(MlFieldArray.to(input));

let x = Field(xv);
let y = Field(yv);
Unsafe: {
/**
* Low-level version of `Poseidon.hashToGroup()`.
*
* **Warning**: This function is marked unsafe because its output is not deterministic.
* It returns the square root of a value without constraining which of the two possible
* square roots is chosen. This allows the prover to choose between two different hashes,
* which can be a vulnerability if consuming code treats the output as unique.
*/
hashToGroup(input: Field[]) {
if (isConstant(input)) {
let result = PoseidonBigint.hashToGroup(toBigints(input));
assert(result !== undefined, 'hashToGroup works on all inputs');
return new Group(result);
}

let x0 = Provable.witness(Field, () => {
// the even root of y^2 will become x0, so the APIs are uniform
let isEven = y.toBigInt() % 2n === 0n;
// y = sqrt(y^2)
let [, x, y] = Snarky.poseidon.hashToGroup(MlFieldArray.to(input));
return new Group({ x, y });
},
},

// we just change the order so the even root is x0
// y.mul(-1); is the second root of sqrt(y^2)
return isEven ? y : y.mul(-1);
});
/**
* Hashes a list of field elements to a point on the Pallas curve.
*
* The output point is deterministic and its discrete log is not efficiently computable.
*/
hashToGroup(input: Field[]): Group {
if (isConstant(input)) return Poseidon.Unsafe.hashToGroup(input);

let x1 = x0.mul(-1);
let { x, y } = Poseidon.Unsafe.hashToGroup(input);

// we check that either x0 or x1 match the original root y
y.equals(x0).or(y.equals(x1)).assertTrue();
// the y coordinate is calculated using a square root, so it has two possible values
// to make the output deterministic, we negate y if it is odd
let sign = Field.from(1n).sub(y.isOdd().toField().mul(2n)); // -1 is y is odd, 1 else
y = y.mul(sign);

return { x, y: { x0, x1 } };
return new Group({ x, y });
},

/**
Expand Down
20 changes: 3 additions & 17 deletions src/lib/provable/test/group.unit-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@ import { Field } from '../field.js';
console.log('group consistency tests');

test(Random.field, Random.scalar, Random.field, (a, s0, x0, assert) => {
const {
x: x1,
y: { x0: y1 },
} = Poseidon.hashToGroup([a])!;
const g = Group.from(x1, y1);
const g = Group(Poseidon.hashToGroup([a])!);

// scale by a scalar
const s = Scalar.from(s0);
Expand All @@ -26,19 +22,9 @@ test(Random.field, Random.scalar, Random.field, (a, s0, x0, assert) => {

// tests consistency between in- and out-circuit implementations
test(Random.field, Random.field, (a, b, assert) => {
const {
x: x1,
y: { x0: y1 },
} = Poseidon.hashToGroup([a])!;

const {
x: x2,
y: { x0: y2 },
} = Poseidon.hashToGroup([b])!;

const zero = Group.zero;
const g1 = Group.from(x1, y1);
const g2 = Group.from(x2, y2);
const g1 = Group(Poseidon.hashToGroup([a])!);
const g2 = Group(Poseidon.hashToGroup([b])!);

run(g1, g2, (x, y) => x.add(y), assert);
run(g1.neg(), g2.neg(), (x, y) => x.add(y), assert);
Expand Down
5 changes: 2 additions & 3 deletions src/mina-signer/src/nullifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ function createNullifier(message: Field[], sk: PrivateKey): Nullifier {

const r = Scalar.random();

const gm = Hash([...message, ...Group.toFields(pk)]);
if (!gm) throw Error('hashToGroup: Point is undefined');
const h_m_pk = { x: gm.x, y: gm.y.x0 };
const h_m_pk = Hash([...message, ...Group.toFields(pk)]);
if (!h_m_pk) throw Error('hashToGroup: Point is undefined');

const nullifier = Group.scale(h_m_pk, sk);
const h_m_pk_r = Group.scale(h_m_pk, r);
Expand Down
4 changes: 2 additions & 2 deletions tests/vk-regression/vk-regression.json
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,8 @@
"digest": "Crypto",
"methods": {
"nullifier": {
"rows": 730,
"digest": "aa8ec2538a8d3133af3f2dd7eb682738"
"rows": 726,
"digest": "64ba0a06a559a3f5a5e68a88c8ec550a"
}
},
"verificationKey": {
Expand Down
Loading