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

Enable custom network signing #1444

Merged
merged 23 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from 10 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
14 changes: 7 additions & 7 deletions src/lib/account-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ import {
protocolVersions,
} from '../bindings/crypto/constants.js';
import { MlArray } from './ml/base.js';
import { Signature, signFieldElement } from '../mina-signer/src/signature.js';
import {
Signature,
signFieldElement,
zkAppBodyPrefix,
} from '../mina-signer/src/signature.js';
import { MlFieldConstArray } from './ml/fields.js';
import {
accountUpdatesToCallForest,
Expand Down Expand Up @@ -1018,9 +1022,7 @@ class AccountUpdate implements Types.AccountUpdate {
if (Provable.inCheckedComputation()) {
let input = Types.AccountUpdate.toInput(this);
return hashWithPrefix(
activeInstance.getNetworkId() === 'mainnet'
? prefixes.zkappBodyMainnet
: prefixes.zkappBodyTestnet,
zkAppBodyPrefix(activeInstance.getNetworkId()),
packToFields(input)
);
} else {
Expand Down Expand Up @@ -1399,9 +1401,7 @@ class AccountUpdate implements Types.AccountUpdate {
function hashAccountUpdate(update: AccountUpdate) {
return genericHash(
AccountUpdate,
activeInstance.getNetworkId() === 'mainnet'
? prefixes.zkappBodyMainnet
: prefixes.zkappBodyTestnet,
zkAppBodyPrefix(activeInstance.getNetworkId()),
update
);
}
Expand Down
21 changes: 13 additions & 8 deletions src/mina-signer/mina-signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,14 @@ export { Client as default };
const defaultValidUntil = '4294967295';

class Client {
private network: NetworkId; // TODO: Rename to "networkId" for consistency with remaining codebase.
private network: NetworkId;

constructor(options: { network: NetworkId }) {
if (!options?.network) {
throw Error('Invalid Specified Network');
}
const specifiedNetwork = options.network.toLowerCase();
Copy link
Member Author

Choose a reason for hiding this comment

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

this is no longer needed since the type now is 'testnet' | 'mainnet' which already is lower case

if (specifiedNetwork !== 'mainnet' && specifiedNetwork !== 'testnet') {
throw Error('Invalid Specified Network');
}

this.network = specifiedNetwork;
}

Expand Down Expand Up @@ -122,9 +120,13 @@ class Client {
* @param privateKey The private key used for signing
* @returns The signed field elements
*/
signFields(fields: bigint[], privateKey: Json.PrivateKey): Signed<bigint[]> {
signFields(
fields: bigint[],
privateKey: Json.PrivateKey,
network?: NetworkId
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved
): Signed<bigint[]> {
let privateKey_ = PrivateKey.fromBase58(privateKey);
let signature = sign({ fields }, privateKey_, 'testnet');
let signature = sign({ fields }, privateKey_, network ?? 'testnet');
return {
signature: Signature.toBase58(signature),
publicKey: PublicKey.toBase58(PrivateKey.toPublicKey(privateKey_)),
Expand All @@ -139,12 +141,15 @@ class Client {
* @returns True if the `signedFields` contains a valid signature matching
* the fields and publicKey.
*/
verifyFields({ data, signature, publicKey }: Signed<bigint[]>) {
verifyFields(
{ data, signature, publicKey }: Signed<bigint[]>,
network?: NetworkId
) {
return verify(
Signature.fromBase58(signature),
{ fields: data },
PublicKey.fromBase58(publicKey),
'testnet'
network ?? 'testnet'
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/mina-signer/src/random-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,6 @@ const RandomTransaction = {
zkappCommand,
zkappCommandAndFeePayerKey,
zkappCommandJson,
networkId: Random.oneOf<NetworkId[]>('testnet', 'mainnet'),
networkId: Random.oneOf<NetworkId[]>('testnet', 'mainnet', 'other'),
accountUpdateWithCallDepth: accountUpdate,
};
13 changes: 1 addition & 12 deletions src/mina-signer/src/sign-zkapp-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
Signature,
signFieldElement,
verifyFieldElement,
zkAppBodyPrefix,
} from './signature.js';
import { mocks } from '../../bindings/crypto/constants.js';
import { NetworkId } from './types.js';
Expand Down Expand Up @@ -159,18 +160,6 @@ function accountUpdatesToCallForest<A extends { body: { callDepth: number } }>(
return forest;
}

const zkAppBodyPrefix = (network: string) => {
Copy link
Member Author

Choose a reason for hiding this comment

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

moved this into mina-signer/src/signature.js for shared use with o1js

switch (network) {
case 'mainnet':
return prefixes.zkappBodyMainnet;
case 'testnet':
return prefixes.zkappBodyTestnet;

default:
return 'ZkappBody' + network;
}
};

function accountUpdateHash(update: AccountUpdate, networkId: NetworkId) {
assertAuthorizationKindValid(update);
let input = AccountUpdate.toInput(update);
Expand Down
102 changes: 52 additions & 50 deletions src/mina-signer/src/sign-zkapp-command.unit-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,42 +82,38 @@ expect(stringify(dummyInput.packed)).toEqual(
stringify(dummyInputSnarky.packed)
);

test(Random.accountUpdate, (accountUpdate) => {
const testnetMinaInstance = Network({
networkId: 'testnet',
mina: 'http://localhost:8080/graphql',
});
const mainnetMinaInstance = Network({
networkId: 'mainnet',
mina: 'http://localhost:8080/graphql',
});

fixVerificationKey(accountUpdate);

// example account update
let accountUpdateJson: Json.AccountUpdate =
AccountUpdate.toJSON(accountUpdate);

// account update hash
let accountUpdateSnarky = AccountUpdateSnarky.fromJSON(accountUpdateJson);
let inputSnarky = TypesSnarky.AccountUpdate.toInput(accountUpdateSnarky);
let input = AccountUpdate.toInput(accountUpdate);
expect(toJSON(input.fields)).toEqual(toJSON(inputSnarky.fields));
expect(toJSON(input.packed)).toEqual(toJSON(inputSnarky.packed));

let packed = packToFields(input);
let packedSnarky = packToFieldsSnarky(inputSnarky);
expect(toJSON(packed)).toEqual(toJSON(packedSnarky));

let hashTestnet = accountUpdateHash(accountUpdate, 'testnet');
let hashMainnet = accountUpdateHash(accountUpdate, 'mainnet');
setActiveInstance(testnetMinaInstance);
let hashSnarkyTestnet = accountUpdateSnarky.hash();
setActiveInstance(mainnetMinaInstance);
let hashSnarkyMainnet = accountUpdateSnarky.hash();
expect(hashTestnet).toEqual(hashSnarkyTestnet.toBigInt());
expect(hashMainnet).toEqual(hashSnarkyMainnet.toBigInt());
});
test(
Copy link
Member Author

Choose a reason for hiding this comment

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

cleaned this up a little by utilizing RandomTransaction.networkId

Copy link
Member

@shimkiv shimkiv Feb 20, 2024

Choose a reason for hiding this comment

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

@Trivo25
Throughout different tests we check that signatures match like testnet <-> testnet but should we also check that the resulting signatures are indeed different for different networks? Like testnet <-> mainnet should always differ.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, we should! And we do
For example, further down we verify against a different network and expect the verification to fail

expect(
   verify(sigFieldElements, networkId === 'mainnet' ? 'testnet' : 'mainnet')
).toEqual(false);

Copy link
Member Author

Choose a reason for hiding this comment

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

but I added a check that the hash doesnt match another networks hash!

Random.accountUpdate,
RandomTransaction.networkId,
(accountUpdate, networkId) => {
const minaInstance = Network({
networkId,
mina: 'http://localhost:8080/graphql',
});

fixVerificationKey(accountUpdate);

// example account update
let accountUpdateJson: Json.AccountUpdate =
AccountUpdate.toJSON(accountUpdate);

// account update hash
let accountUpdateSnarky = AccountUpdateSnarky.fromJSON(accountUpdateJson);
let inputSnarky = TypesSnarky.AccountUpdate.toInput(accountUpdateSnarky);
let input = AccountUpdate.toInput(accountUpdate);
expect(toJSON(input.fields)).toEqual(toJSON(inputSnarky.fields));
expect(toJSON(input.packed)).toEqual(toJSON(inputSnarky.packed));

let packed = packToFields(input);
let packedSnarky = packToFieldsSnarky(inputSnarky);
expect(toJSON(packed)).toEqual(toJSON(packedSnarky));

setActiveInstance(minaInstance);
let hashSnarky = accountUpdateSnarky.hash();
let hash = accountUpdateHash(accountUpdate, networkId);
expect(hash).toEqual(hashSnarky.toBigInt());
}
);

// private key to/from base58
test(Random.json.privateKey, (feePayerKeyBase58) => {
Expand All @@ -140,19 +136,25 @@ test(memoGenerator, (memoString) => {
});

// zkapp transaction - basic properties & commitment
test(RandomTransaction.zkappCommand, (zkappCommand, assert) => {
zkappCommand.accountUpdates.forEach(fixVerificationKey);

assert(isCallDepthValid(zkappCommand));
let zkappCommandJson = ZkappCommand.toJSON(zkappCommand);
let ocamlCommitments = Test.hashFromJson.transactionCommitments(
JSON.stringify(zkappCommandJson),
'testnet'
);
let callForest = accountUpdatesToCallForest(zkappCommand.accountUpdates);
let commitment = callForestHash(callForest, 'testnet');
expect(commitment).toEqual(FieldConst.toBigint(ocamlCommitments.commitment));
});
test(
Copy link
Member Author

Choose a reason for hiding this comment

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

this now tests all network id types

RandomTransaction.zkappCommand,
RandomTransaction.networkId,
(zkappCommand, networkId, assert) => {
zkappCommand.accountUpdates.forEach(fixVerificationKey);

assert(isCallDepthValid(zkappCommand));
let zkappCommandJson = ZkappCommand.toJSON(zkappCommand);
let ocamlCommitments = Test.hashFromJson.transactionCommitments(
JSON.stringify(zkappCommandJson),
networkId
);
let callForest = accountUpdatesToCallForest(zkappCommand.accountUpdates);
let commitment = callForestHash(callForest, networkId);
expect(commitment).toEqual(
FieldConst.toBigint(ocamlCommitments.commitment)
);
}
);

// invalid zkapp transactions
test.negative(
Expand Down Expand Up @@ -242,7 +244,7 @@ test(
let sigOCaml = Test.signature.signFieldElement(
ocamlCommitments.fullCommitment,
Ml.fromPrivateKey(feePayerKeySnarky),
networkId === 'mainnet' ? true : false
networkId
);

expect(Signature.toBase58(sigFieldElements)).toEqual(sigOCaml);
Expand Down
78 changes: 66 additions & 12 deletions src/mina-signer/src/signature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export {
signLegacy,
verifyLegacy,
deriveNonce,
signaturePrefix,
zkAppBodyPrefix,
};

const networkIdMainnet = 0x01n;
Expand Down Expand Up @@ -150,10 +152,10 @@ function deriveNonce(
): Scalar {
let { x, y } = publicKey;
let d = Field(privateKey);
let id = networkId === 'mainnet' ? networkIdMainnet : networkIdTestnet;
let id = getNetworkIdHashInput(networkId);
let input = HashInput.append(message, {
fields: [x, y, d],
packed: [[id, 8]],
packed: [id],
});
let packedInput = packToFields(input);
let inputBits = packedInput.map(Field.toBits).flat();
Expand Down Expand Up @@ -189,11 +191,7 @@ function hashMessage(
): Scalar {
let { x, y } = publicKey;
let input = HashInput.append(message, { fields: [x, y, r] });
let prefix =
networkId === 'mainnet'
? prefixes.signatureMainnet
: prefixes.signatureTestnet;
return hashWithPrefix(prefix, packToFields(input));
return hashWithPrefix(signaturePrefix(networkId), packToFields(input));
}

/**
Expand Down Expand Up @@ -280,7 +278,7 @@ function deriveNonceLegacy(
): Scalar {
let { x, y } = publicKey;
let scalarBits = Scalar.toBits(privateKey);
let id = networkId === 'mainnet' ? networkIdMainnet : networkIdTestnet;
let id = getNetworkIdHashInput(networkId)[0];
let idBits = bytesToBits([Number(id)]);
let input = HashInputLegacy.append(message, {
fields: [x, y],
Expand Down Expand Up @@ -311,9 +309,65 @@ function hashMessageLegacy(
): Scalar {
let { x, y } = publicKey;
let input = HashInputLegacy.append(message, { fields: [x, y, r], bits: [] });
let prefix =
networkId === 'mainnet'
? prefixes.signatureMainnet
: prefixes.signatureTestnet;
let prefix = signaturePrefix(networkId);
return HashLegacy.hashWithPrefix(prefix, packToFieldsLegacy(input));
}

const numberToBytePadded = (b: number) => b.toString(2).padStart(8, '0');

function networkIdOfString(n: string): [bigint, number] {
let l = n.length;
let acc = '';
for (let i = l - 1; i >= 0; i--) {
let b = n.charCodeAt(i);
let padded = numberToBytePadded(b);
acc = acc.concat(padded);
}
return [BigInt('0b' + acc), acc.length];
}

function getNetworkIdHashInput(networkId: string): [bigint, number] {
switch (networkId) {
case 'mainnet':
return [networkIdMainnet, 8];
case 'testnet':
return [networkIdTestnet, 8];
default:
return networkIdOfString(networkId);
}
}

const createCustomPrefix = (prefix: string) => {
Copy link
Member Author

Choose a reason for hiding this comment

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

OCaml uses a * padding to 20 bytes

const maxLength = 20;
const paddingChar = '*';
let length = prefix.length;

if (length <= maxLength) {
let diff = maxLength - length;
return prefix + paddingChar.repeat(diff);
} else {
return prefix.substring(0, maxLength);
}
};

const signaturePrefix = (network: string) => {
switch (network) {
case 'mainnet':
return prefixes.signatureMainnet;
case 'testnet':
return prefixes.signatureTestnet;
default:
return createCustomPrefix(network + 'Signature');
}
};

const zkAppBodyPrefix = (network: string) => {
switch (network) {
case 'mainnet':
return prefixes.zkappBodyMainnet;
case 'testnet':
return prefixes.zkappBodyTestnet;
default:
return createCustomPrefix(network + 'ZkappBody');
}
};
Loading
Loading