Skip to content

Commit

Permalink
Merge pull request #999 from o1-labs/feat/add-sha
Browse files Browse the repository at this point in the history
Add new hashing functions (SHA & Keccak)
  • Loading branch information
mitschabaude authored Dec 14, 2023
2 parents 14fdf68 + 8d52779 commit 004112d
Show file tree
Hide file tree
Showing 26 changed files with 2,068 additions and 186 deletions.
2 changes: 1 addition & 1 deletion src/bindings
47 changes: 8 additions & 39 deletions src/examples/crypto/ecdsa/ecdsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,34 @@ import {
createEcdsa,
createForeignCurve,
Bool,
Struct,
Provable,
Field,
Keccak,
Gadgets,
Bytes,
} from 'o1js';

export { keccakAndEcdsa, ecdsa, Secp256k1, Ecdsa, Message32 };
export { keccakAndEcdsa, ecdsa, Secp256k1, Ecdsa, Bytes32 };

class Secp256k1 extends createForeignCurve(Crypto.CurveParams.Secp256k1) {}
class Scalar extends Secp256k1.Scalar {}
class Ecdsa extends createEcdsa(Secp256k1) {}
class Message32 extends Message(32) {}
class Bytes32 extends Bytes(32) {}

const keccakAndEcdsa = ZkProgram({
name: 'ecdsa',
publicInput: Message32,
publicInput: Bytes32.provable,
publicOutput: Bool,

methods: {
verifyEcdsa: {
privateInputs: [Ecdsa.provable, Secp256k1.provable],
method(message: Message32, signature: Ecdsa, publicKey: Secp256k1) {
return signature.verify(message.array, publicKey);
method(message: Bytes32, signature: Ecdsa, publicKey: Secp256k1) {
return signature.verify(message, publicKey);
},
},

sha3: {
privateInputs: [],
method(message: Message32) {
Keccak.nistSha3(256, message.array);
method(message: Bytes32) {
Keccak.nistSha3(256, message);
return Bool(true);
},
},
Expand All @@ -55,31 +52,3 @@ const ecdsa = ZkProgram({
},
},
});

// helper: class for a message of n bytes

function Message(lengthInBytes: number) {
return class Message extends Struct({
array: Provable.Array(Field, lengthInBytes),
}) {
static from(message: string | Uint8Array) {
if (typeof message === 'string') {
message = new TextEncoder().encode(message);
}
let padded = new Uint8Array(32);
padded.set(message);
return new this({ array: [...padded].map(Field) });
}

toBytes() {
return Uint8Array.from(this.array.map((f) => Number(f)));
}

/**
* Important: check that inputs are, in fact, bytes
*/
static check(msg: { array: Field[] }) {
msg.array.forEach(Gadgets.rangeCheck8);
}
};
}
4 changes: 2 additions & 2 deletions src/examples/crypto/ecdsa/run.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Secp256k1, Ecdsa, keccakAndEcdsa, Message32, ecdsa } from './ecdsa.js';
import { Secp256k1, Ecdsa, keccakAndEcdsa, ecdsa, Bytes32 } from './ecdsa.js';
import assert from 'assert';

// create an example ecdsa signature

let privateKey = Secp256k1.Scalar.random();
let publicKey = Secp256k1.generator.scale(privateKey);

let message = Message32.from("what's up");
let message = Bytes32.fromString("what's up");

let signature = Ecdsa.sign(message.toBytes(), privateKey.toBigInt());

Expand Down
49 changes: 49 additions & 0 deletions src/examples/zkapps/hashing/hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {
Hash,
Field,
SmartContract,
state,
State,
method,
Permissions,
Bytes,
} from 'o1js';

let initialCommitment: Field = Field(0);

export class HashStorage extends SmartContract {
@state(Field) commitment = State<Field>();

init() {
super.init();
this.account.permissions.set({
...Permissions.default(),
editState: Permissions.proofOrSignature(),
});
this.commitment.set(initialCommitment);
}

@method SHA256(xs: Bytes) {
const shaHash = Hash.SHA3_256.hash(xs);
const commitment = Hash.hash(shaHash.toFields());
this.commitment.set(commitment);
}

@method SHA384(xs: Bytes) {
const shaHash = Hash.SHA3_384.hash(xs);
const commitment = Hash.hash(shaHash.toFields());
this.commitment.set(commitment);
}

@method SHA512(xs: Bytes) {
const shaHash = Hash.SHA3_512.hash(xs);
const commitment = Hash.hash(shaHash.toFields());
this.commitment.set(commitment);
}

@method Keccak256(xs: Bytes) {
const shaHash = Hash.Keccak256.hash(xs);
const commitment = Hash.hash(shaHash.toFields());
this.commitment.set(commitment);
}
}
73 changes: 73 additions & 0 deletions src/examples/zkapps/hashing/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { HashStorage } from './hash.js';
import { Mina, PrivateKey, AccountUpdate, Bytes } from 'o1js';

let txn;
let proofsEnabled = true;
// setup local ledger
let Local = Mina.LocalBlockchain({ proofsEnabled });
Mina.setActiveInstance(Local);

if (proofsEnabled) {
console.log('Proofs enabled');
HashStorage.compile();
}

// test accounts that pays all the fees, and puts additional funds into the zkapp
const feePayer = Local.testAccounts[0];

// zkapp account
const zkAppPrivateKey = PrivateKey.random();
const zkAppAddress = zkAppPrivateKey.toPublicKey();
const zkAppInstance = new HashStorage(zkAppAddress);

// 0, 1, 2, 3, ..., 32
const hashData = Bytes.from(Array.from({ length: 32 }, (_, i) => i));

console.log('Deploying Hash Example....');
txn = await Mina.transaction(feePayer.publicKey, () => {
AccountUpdate.fundNewAccount(feePayer.publicKey);
zkAppInstance.deploy();
});
await txn.sign([feePayer.privateKey, zkAppPrivateKey]).send();

const initialState =
Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString();

let currentState;
console.log('Initial State', initialState);

console.log(`Updating commitment from ${initialState} using SHA256 ...`);
txn = await Mina.transaction(feePayer.publicKey, () => {
zkAppInstance.SHA256(hashData);
});
await txn.prove();
await txn.sign([feePayer.privateKey]).send();
currentState = Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString();
console.log(`Current state successfully updated to ${currentState}`);

console.log(`Updating commitment from ${initialState} using SHA384 ...`);
txn = await Mina.transaction(feePayer.publicKey, () => {
zkAppInstance.SHA384(hashData);
});
await txn.prove();
await txn.sign([feePayer.privateKey]).send();
currentState = Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString();
console.log(`Current state successfully updated to ${currentState}`);

console.log(`Updating commitment from ${initialState} using SHA512 ...`);
txn = await Mina.transaction(feePayer.publicKey, () => {
zkAppInstance.SHA512(hashData);
});
await txn.prove();
await txn.sign([feePayer.privateKey]).send();
currentState = Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString();
console.log(`Current state successfully updated to ${currentState}`);

console.log(`Updating commitment from ${initialState} using Keccak256...`);
txn = await Mina.transaction(feePayer.publicKey, () => {
zkAppInstance.Keccak256(hashData);
});
await txn.prove();
await txn.sign([feePayer.privateKey]).send();
currentState = Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString();
console.log(`Current state successfully updated to ${currentState}`);
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export { createForeignCurve, ForeignCurve } from './lib/foreign-curve.js';
export { createEcdsa, EcdsaSignature } from './lib/foreign-ecdsa.js';
export { Poseidon, TokenSymbol } from './lib/hash.js';
export { Keccak } from './lib/keccak.js';
export { Hash } from './lib/hashes-combined.js';

export * from './lib/signature.js';
export type {
Expand All @@ -31,7 +32,8 @@ export {
} from './lib/circuit_value.js';
export { Provable } from './lib/provable.js';
export { Circuit, Keypair, public_, circuitMain } from './lib/circuit.js';
export { UInt32, UInt64, Int64, Sign } from './lib/int.js';
export { UInt32, UInt64, Int64, Sign, UInt8 } from './lib/int.js';
export { Bytes } from './lib/provable-types/provable-types.js';
export { Gadgets } from './lib/gadgets/gadgets.js';
export { Types } from './bindings/mina-transaction/types.js';

Expand Down
19 changes: 10 additions & 9 deletions src/lib/foreign-ecdsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import { AlmostForeignField } from './foreign-field.js';
import { assert } from './gadgets/common.js';
import { Field3 } from './gadgets/foreign-field.js';
import { Ecdsa } from './gadgets/elliptic-curve.js';
import { Field } from './field.js';
import { l } from './gadgets/range-check.js';
import { Keccak } from './keccak.js';
import { Bytes } from './provable-types/provable-types.js';
import { UInt8 } from './int.js';

// external API
export { createEcdsa, EcdsaSignature };
Expand Down Expand Up @@ -99,7 +100,7 @@ class EcdsaSignature {
* isValid.assertTrue('signature verifies');
* ```
*/
verify(message: Field[], publicKey: FlexiblePoint) {
verify(message: Bytes, publicKey: FlexiblePoint) {
let msgHashBytes = Keccak.ethereum(message);
let msgHash = keccakOutputToScalar(msgHashBytes, this.Constructor.Curve);
return this.verifySignedHash(msgHash, publicKey);
Expand Down Expand Up @@ -132,8 +133,7 @@ class EcdsaSignature {
* Note: This method is not provable, and only takes JS bigints as input.
*/
static sign(message: (bigint | number)[] | Uint8Array, privateKey: bigint) {
let msgFields = [...message].map(Field.from);
let msgHashBytes = Keccak.ethereum(msgFields);
let msgHashBytes = Keccak.ethereum(message);
let msgHash = keccakOutputToScalar(msgHashBytes, this.Curve);
return this.signHash(msgHash.toBigInt(), privateKey);
}
Expand Down Expand Up @@ -228,7 +228,7 @@ function toObject(signature: EcdsaSignature) {
* - takes a 32 bytes hash
* - converts them to 3 limbs which collectively have L_n <= 256 bits
*/
function keccakOutputToScalar(hash: Field[], Curve: typeof ForeignCurve) {
function keccakOutputToScalar(hash: Bytes, Curve: typeof ForeignCurve) {
const L_n = Curve.Scalar.sizeInBits;
// keep it simple for now, avoid dealing with dropping bits
// TODO: what does "leftmost bits" mean? big-endian or little-endian?
Expand All @@ -240,14 +240,15 @@ function keccakOutputToScalar(hash: Field[], Curve: typeof ForeignCurve) {
// piece together into limbs
// bytes are big-endian, so the first byte is the most significant
assert(l === 88n);
let x2 = bytesToLimbBE(hash.slice(0, 10));
let x1 = bytesToLimbBE(hash.slice(10, 21));
let x0 = bytesToLimbBE(hash.slice(21, 32));
let x2 = bytesToLimbBE(hash.bytes.slice(0, 10));
let x1 = bytesToLimbBE(hash.bytes.slice(10, 21));
let x0 = bytesToLimbBE(hash.bytes.slice(21, 32));

return new Curve.Scalar.AlmostReduced([x0, x1, x2]);
}

function bytesToLimbBE(bytes: Field[]) {
function bytesToLimbBE(bytes_: UInt8[]) {
let bytes = bytes_.map((x) => x.value);
let n = bytes.length;
let limb = bytes[0];
for (let i = 1; i < n; i++) {
Expand Down
10 changes: 10 additions & 0 deletions src/lib/gadgets/gadgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import {
compactMultiRangeCheck,
multiRangeCheck,
rangeCheck16,
rangeCheck64,
rangeCheck8,
} from './range-check.js';
Expand Down Expand Up @@ -41,6 +42,15 @@ const Gadgets = {
return rangeCheck64(x);
},

/**
* Asserts that the input value is in the range [0, 2^16).
*
* See {@link Gadgets.rangeCheck64} for analogous details and usage examples.
*/
rangeCheck16(x: Field) {
return rangeCheck16(x);
},

/**
* Asserts that the input value is in the range [0, 2^8).
*
Expand Down
20 changes: 19 additions & 1 deletion src/lib/gadgets/range-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { Field } from '../field.js';
import { Gates } from '../gates.js';
import { assert, bitSlice, exists, toVar, toVars } from './common.js';

export { rangeCheck64, rangeCheck8, multiRangeCheck, compactMultiRangeCheck };
export {
rangeCheck64,
rangeCheck8,
rangeCheck16,
multiRangeCheck,
compactMultiRangeCheck,
};
export { l, l2, l3, lMask, l2Mask };

/**
Expand Down Expand Up @@ -208,6 +214,18 @@ function rangeCheck1Helper(inputs: {
);
}

function rangeCheck16(x: Field) {
if (x.isConstant()) {
assert(
x.toBigInt() < 1n << 16n,
`rangeCheck16: expected field to fit in 8 bits, got ${x}`
);
return;
}
// check that x fits in 16 bits
x.rangeCheckHelper(16).assertEquals(x);
}

function rangeCheck8(x: Field) {
if (x.isConstant()) {
assert(
Expand Down
21 changes: 19 additions & 2 deletions src/lib/gadgets/test-utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import type { FiniteField } from '../../bindings/crypto/finite_field.js';
import { ProvableSpec } from '../testing/equivalent.js';
import { ProvableSpec, spec } from '../testing/equivalent.js';
import { Random } from '../testing/random.js';
import { Gadgets } from './gadgets.js';
import { assert } from './common.js';
import { Bytes } from '../provable-types/provable-types.js';

export { foreignField, unreducedForeignField, uniformForeignField, throwError };
export {
foreignField,
unreducedForeignField,
uniformForeignField,
bytes,
throwError,
};

const { Field3 } = Gadgets;

Expand Down Expand Up @@ -49,6 +56,16 @@ function uniformForeignField(
};
}

function bytes(length: number) {
const Bytes_ = Bytes(length);
return spec<Uint8Array, Bytes>({
rng: Random.map(Random.bytes(length), (x) => Uint8Array.from(x)),
there: Bytes_.from,
back: (x) => x.toBytes(),
provable: Bytes_.provable,
});
}

// helper

function throwError<T>(message: string): T {
Expand Down
Loading

0 comments on commit 004112d

Please sign in to comment.