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

Mina-signer: in-SNARK verifiable Signatures #710

Merged
merged 10 commits into from
Feb 10, 2023

Conversation

mitschabaude
Copy link
Collaborator

@mitschabaude mitschabaude commented Jan 30, 2023

closes MinaProtocol/mina#11327

this is about letting users sign data with mina-signer (i.e., with their wallet), such that the signature can be verified by a smart contract. in other words, we need to make sure there is a signing function in mina-signer whose signatures can be verified in-snark by snarkyjs.

  • adds signFields and verifyFields to mina-signer
  • modifies the existing Signature.create and Signature.verify in snarkyjs, to be compatible with signFields
    • note that Signature.create is not for in-snark usage, but Signature.verify is

Making snarkyjs signatures compatible uncovered a bug/irregularity in the previous implementation:

  • after hashing the message to be signed, the algorithm reinterprets the hash (a base field element) as a scalar
  • in snarkyjs, this was done with Scalar.fromBits(hash.toBits())
  • however, it turned out that Scalar uses a "shifted" representation and fromBits just interprets the bits to already be in shifted form
  • so, it can be said that the existing algorithm used a non-standard way of moving from base field to scalar
  • this is fixed here, by unshifting the scalar. in the case of the in-snark verify, we do the unshifting on the group element level, leveraging the existing Group.scale method. it's not clear to me if there's a more efficient way to do this

@mitschabaude mitschabaude marked this pull request as ready for review January 31, 2023 13:30
Copy link
Collaborator Author

@mitschabaude mitschabaude left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some comments to help with review:

@@ -1877,7 +1877,11 @@ function signJsonTransaction(
accountUpdate.authorization.proof === null
) {
zkappCommand = JSON.parse(
Ledger.signAccountUpdate(JSON.stringify(zkappCommand), privateKey, i)
Ledger.signOtherAccountUpdate(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unrelated fix

@@ -72,3 +74,8 @@ That means it can't be called in a @method or similar environment, and there's n
highBit: Bool(x >> lowBitSize === 1n),
};
};

Scalar.fromBigInt = function (scalar: bigint) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing helper which was useful here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same question as elsewhere, I'm curious how a dev would know not to use these functions if they want them to create constraints? My guess is that we're warning them that going to bigint or from bigint is not constraining anything?

Copy link
Collaborator Author

@mitschabaude mitschabaude Feb 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For methods like these, which take a JS value and produce constant field elements, I think no warning is needed -- it's a valid and useful way to introduce constants into the circuit.

Going the other way -- from field elements to JS value -- is a very common mistake, but throws an error as soon as you try to compile the contract. That's because extracting values, under the hood, always works like this:

match x with
  | Constant x -> x
  | x -> As_prover.read_var x

so during compile time you'll hit an error for the As_prover.read_var x case. The constant case is fine - no need to prove how constants are computed, since the prover can't change them

@@ -192,12 +198,23 @@ class Signature extends CircuitValue {
static create(privKey: PrivateKey, msg: Field[]): Signature {
const publicKey = PublicKey.fromPrivateKey(privKey).toGroup();
const d = privKey.s;
const kPrime = Scalar.random();
const kPrime = Scalar.fromBigInt(
deriveNonce(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crypto change: instead of a random nonce, we now use the same deterministic nonce derivation as in mina-signer (and elsewhere in our protocol)

let { x: r, y: ry } = Group.generator.scale(kPrime);
const k = ry.toBits()[0].toBoolean() ? kPrime.neg() : kPrime;
const e = Scalar.fromBits(
Poseidon.hash(msg.concat([publicKey.x, publicKey.y, r])).toBits()
let h = hashWithPrefix(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crypto change: instead of plain hashing with Poseidon, we use a hash prefix

const e = Scalar.fromBits(
Poseidon.hash(msg.concat([publicKey.x, publicKey.y, r])).toBits()
let h = hashWithPrefix(
prefixes.signatureTestnet,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chose to hard-code the signature to 'testnet', since the distinction between testnet and mainnet for signatures which aren't about transactions seemed not useful to me and just adds noise IMO

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is actually still very important if these signatures are going to be used in zkApps -- the same security vulnerability applies: If I sign something on the testnet, it could be replayed on mainnet and my funds can be drained if I use the same keypairs/nonce.

Copy link
Collaborator Author

@mitschabaude mitschabaude Jan 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point! It opens a can of worms though. When signing transactions, the signature verifier is the Mina node, which has a well-defined network - it's either a mainnet node or a testnet node, so it knows which one it should use for verifying.
However, a smart contract currently doesn't have that. Do you think the network type should be hard-coded into the smart contract / circuit somehow? Should we leave it as a constant for the developer to define? (But then we're pushing to them the problem of switching this flag when deploying / compiling to either network).

I think, if we go this route, the network type should definitely be a constant in the circuit and not a Bool that the developer can make variable. Because if it can be variable, it's too easy a mistake to just make it a private user input which doesn't constrain the proofs to be valid for the respective network.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I suppose it’s much less dangerous than a transaction so I think it’s not worth the complexity cost of making these signatures distinct, but we should probably run this by more people. I’ll approve but I’m going to send a link to one of our slack channels and let’s let it bake overnight if anyone disagrees before we land it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

am I understanding this right that the constant doesn't matter here because it's what the zkapp will verify and so you could pretty much use anything?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mimoo yes, doesn't matter here in the sense that these signatures won't be sent to either network. we are completely in user-land: signatures are inputs to proofs created locally, where they are verified in-snark.

I think Brandon highlights an important problem: we don't provide replay protection for the user here, especially given that signatures are deterministic (nonce is derived from the message and private key). so, we currently leave it up to the application to figure out how to prevent that.

I think its too deep and broad a problem to solve in the context of this PR, but I created an issue so we don't forget to discuss this: #720

Comment on lines +215 to +217
// TODO: Scalar.fromBits interprets the input as a "shifted scalar"
// therefore we have to unshift e before using it
let e = unshift(Scalar.fromBits(h.toBits()));
Copy link
Collaborator Author

@mitschabaude mitschabaude Jan 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crypto change: So this seems like a bug in Scalar.fromBits, which is somewhat hard to resolve in that function directly, since this function would need to unshift the scalar in a SNARK-compatible way. It seemed easier to unshift out of the snark (here) and do the unshifting at the level of curve points when we're in the snark (see verify below)

Comment on lines +259 to +264
// performs scalar multiplication s*G assuming that instead of s, we got s' = 2s + 1 + 2^255
// cost: 2x scale by constant, 1x scale by variable
function scaleShifted(point: Group, shiftedScalar: Scalar) {
let oneHalfGroup = point.scale(Scalar.fromBigInt(oneHalf));
let shiftGroup = oneHalfGroup.scale(Scalar.fromBigInt(shift));
return oneHalfGroup.scale(shiftedScalar).sub(shiftGroup);
Copy link
Collaborator Author

@mitschabaude mitschabaude Jan 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crypto change: this is how we undo the scalar shift inside the SNARK

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just do

point.scale((shiftedScalar.sub(shift).mul(inv_2))

Copy link
Contributor

@mimoo mimoo Feb 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh I think I see, the scalars are in the wrong field :D that sucks

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep exactly :D we don't have the foreign field arithmetic to do sub and mul on the scalar, inside the snark

Comment on lines +116 to +118
signFields(fields: bigint[], privateKey: Json.PrivateKey): Signed<bigint[]> {
let privateKey_ = PrivateKey.fromBase58(privateKey);
let signature = sign({ fields }, privateKey_, 'testnet');
Copy link
Collaborator Author

@mitschabaude mitschabaude Jan 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crypto change: very straight-forward new function in mina-signer to sign an arbitrary list of field elements. it uses the existing sign and hard-codes the network to 'testnet' since this is not a transaction signature

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it makes sense to hardcode the chain id to 'zkapp' or something else then?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it does! Do you think we need actual domain separation between testnet signatures and signatures meant for zkapps? Or is it more about better naming, so that 'zkapp' is an alias for 'testnet'? (it wouldn't be hard to add the real domain separation fwiw)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry for late reply. In general I would say we should use domain separate as much as possible, because it's cheap/free : ) it's possible that it's not exploitable, but still why not distinguish it

@@ -0,0 +1,49 @@
import { Field, isReady, shutdown } from '../../snarky.js';
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this file tests that the new signatures are really compatible between mina-signer and snarkyjs, and also checks that they can be verified inside a SNARK

const e = Scalar.fromBits(
Poseidon.hash(msg.concat([publicKey.x, publicKey.y, r])).toBits()
let h = hashWithPrefix(
prefixes.signatureTestnet,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is actually still very important if these signatures are going to be used in zkApps -- the same security vulnerability applies: If I sign something on the testnet, it could be replayed on mainnet and my funds can be drained if I use the same keypairs/nonce.

@@ -1274,7 +1279,7 @@ declare class Ledger {
/**
* Signs an account update.
*/
static signAccountUpdate(
static signOtherAccountUpdate(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did this name change?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an unrelated fix, the name was actually signOtherAccountUpdate in the ocaml bindings, so it didn't work before

Copy link
Member

@bkase bkase left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Product okay (tentatively, see comment). Still needs a crypto review from someone

Copy link
Contributor

@mimoo mimoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

const e = Scalar.fromBits(
Poseidon.hash(msg.concat([publicKey.x, publicKey.y, r])).toBits()
let h = hashWithPrefix(
prefixes.signatureTestnet,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

am I understanding this right that the constant doesn't matter here because it's what the zkapp will verify and so you could pretty much use anything?

Comment on lines +116 to +118
signFields(fields: bigint[], privateKey: Json.PrivateKey): Signed<bigint[]> {
let privateKey_ = PrivateKey.fromBase58(privateKey);
let signature = sign({ fields }, privateKey_, 'testnet');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it makes sense to hardcode the chain id to 'zkapp' or something else then?

@@ -72,3 +74,8 @@ That means it can't be called in a @method or similar environment, and there's n
highBit: Bool(x >> lowBitSize === 1n),
};
};

Scalar.fromBigInt = function (scalar: bigint) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same question as elsewhere, I'm curious how a dev would know not to use these functions if they want them to create constraints? My guess is that we're warning them that going to bigint or from bigint is not constraining anything?

}

let shift = ScalarBigint(1n + 2n ** 255n);
let oneHalf = ScalarBigint.inverse(2n)!;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't you do let oneHalf = Scalar.fromBigInt(ScalarBigint.inverse(2n));?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no.. because Scalar.fromBigInt needs the JSOO / Wasm bundle to be loaded, and that's not the case at the top level. (only after await isReady)

Comment on lines +259 to +264
// performs scalar multiplication s*G assuming that instead of s, we got s' = 2s + 1 + 2^255
// cost: 2x scale by constant, 1x scale by variable
function scaleShifted(point: Group, shiftedScalar: Scalar) {
let oneHalfGroup = point.scale(Scalar.fromBigInt(oneHalf));
let shiftGroup = oneHalfGroup.scale(Scalar.fromBigInt(shift));
return oneHalfGroup.scale(shiftedScalar).sub(shiftGroup);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just do

point.scale((shiftedScalar.sub(shift).mul(inv_2))

let r = point.scale(e).neg().add(Group.generator.scale(this.s));
// TODO: Scalar.fromBits interprets the input as a "shifted scalar"
// therefore we have to use scaleShifted which is very inefficient
let e = Scalar.fromBits(h.toBits());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this going to create constraints?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah sure! toBits does the proper in-snark unpacking of a field element, and fromBits is a no-op, since scalars are already represented as an array of bits (unfortunately, the bits represent a shifted scalar :/)

function unshift(shiftedScalar: Scalar) {
return shiftedScalar
.sub(Scalar.fromBigInt(shift))
.mul(Scalar.fromBigInt(oneHalf));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does this only work out of snark? Also, isn't this a problem if I want to sign within a snark? (since it's used in the signature algo)

Copy link
Collaborator Author

@mitschabaude mitschabaude Feb 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only works out of snark because sub and mul are not available on variable scalars. not sure how efficient they would be to implement in-snark, compared to the extra scaling I have to do instead. do you know?

the signing algorithm was already (I think, deliberately) written to not work inside a snark (for example, it used a constant random field element for the nonce 😱 ; now I'm deriving the nonce as a blake2b hash. tbf, the nonce could be a prover witness). the above will throw an error in the snark.

Inherently, signing in the snark is not as useful because then you have to provide your private key as input -- but that usually sits in some guarded place like your wallet. so, I think it's fine to only support signing outside + verifying inside which achieves the same and plays well with wallets

@mitschabaude mitschabaude merged commit 8109a64 into main Feb 10, 2023
@mitschabaude mitschabaude deleted the mina-signer/snark-compatible-signing branch February 10, 2023 16:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add signatures to mina-signer which can be verified in snarkyjs circuits
3 participants