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: sign account updates #705

Merged
merged 7 commits into from
Jan 30, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased](https://github.com/o1-labs/snarkyjs/compare/c5a36207...HEAD)

> No unreleased changes yet
### Added

- `Transaction.fromJSON` to recover transaction object from JSON https://github.com/o1-labs/snarkyjs/pull/705

## [0.8.0](https://github.com/o1-labs/snarkyjs/compare/d880bd6e...c5a36207)

Expand Down
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ export {
AccountUpdate,
Permissions,
ZkappPublicInput,
zkappCommandToJson,
} from './lib/account_update.js';

export {
Expand Down
27 changes: 18 additions & 9 deletions src/lib/account_update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
memoizeWitness,
FlexibleProvable,
} from './circuit_value.js';
import { Field, Bool, Ledger, Circuit, Pickles, Provable } from '../snarky.js';
import { Field, Bool, Ledger, Circuit, Pickles } from '../snarky.js';
import { jsLayout } from '../provable/gen/js-layout.js';
import { Types, toJSONEssential } from '../provable/types.js';
import { PrivateKey, PublicKey } from './signature.js';
Expand All @@ -19,6 +19,7 @@ import { hashWithPrefix, packToFields, prefixes, TokenSymbol } from './hash.js';
import * as Encoding from './encoding.js';
import { Context } from './global-context.js';
import { Events, SequenceEvents } from '../provable/transaction-leaves.js';
import { Memo } from '../mina-signer/src/memo.js';

// external API
export { Permissions, AccountUpdate, ZkappPublicInput };
Expand All @@ -33,7 +34,6 @@ export {
Authorization,
FeePayerUnsigned,
ZkappCommand,
zkappCommandToJson,
addMissingSignatures,
addMissingProofs,
signJsonTransaction,
Expand Down Expand Up @@ -1615,7 +1615,7 @@ type ZkappCommandProved = {

const ZkappCommand = {
toPretty(transaction: ZkappCommand) {
let feePayer = zkappCommandToJson(transaction).feePayer as any;
let feePayer = ZkappCommand.toJSON(transaction).feePayer as any;
feePayer.body.publicKey = '..' + feePayer.body.publicKey.slice(-4);
feePayer.body.authorization = '..' + feePayer.authorization.slice(-4);
if (feePayer.body.validUntil === null) delete feePayer.body.validUntil;
Expand All @@ -1624,13 +1624,22 @@ const ZkappCommand = {
...transaction.accountUpdates.map((a) => a.toPretty()),
];
},
fromJSON(json: Types.Json.ZkappCommand): ZkappCommand {
let { feePayer } = Types.ZkappCommand.fromJSON({
feePayer: json.feePayer,
accountUpdates: [],
memo: json.memo,
});
let memo = Memo.toString(Memo.fromBase58(json.memo));
let accountUpdates = json.accountUpdates.map(AccountUpdate.fromJSON);
return { feePayer, accountUpdates, memo };
},
toJSON({ feePayer, accountUpdates, memo }: ZkappCommand) {
memo = Ledger.memoToBase58(memo);
return Types.ZkappCommand.toJSON({ feePayer, accountUpdates, memo });
},
};

function zkappCommandToJson({ feePayer, accountUpdates, memo }: ZkappCommand) {
memo = Ledger.memoToBase58(memo);
return Types.ZkappCommand.toJSON({ feePayer, accountUpdates, memo });
}

const Authorization = {
hasLazyProof(accountUpdate: AccountUpdate) {
return accountUpdate.lazyAuthorization?.kind === 'lazy-proof';
Expand Down Expand Up @@ -1677,7 +1686,7 @@ function addMissingSignatures(
): ZkappCommandSigned {
let additionalPublicKeys = additionalKeys.map((sk) => sk.toPublicKey());
let { commitment, fullCommitment } = Ledger.transactionCommitments(
JSON.stringify(zkappCommandToJson(zkappCommand))
JSON.stringify(ZkappCommand.toJSON(zkappCommand))
);
function addFeePayerSignature(accountUpdate: FeePayerUnsigned): FeePayer {
let { body, authorization, lazyAuthorization } =
Expand Down
40 changes: 26 additions & 14 deletions src/lib/mina.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// This is for an account where any of a list of public keys can update the state

import { Circuit, Ledger, LedgerAccount } from '../snarky.js';
import { Field, Bool } from './core.js';
import { UInt32, UInt64 } from './int.js';
Expand All @@ -9,7 +7,6 @@ import {
addMissingSignatures,
FeePayerUnsigned,
ZkappCommand,
zkappCommandToJson,
AccountUpdate,
ZkappStateLength,
ZkappPublicInput,
Expand Down Expand Up @@ -37,6 +34,7 @@ export {
LocalBlockchain,
currentTransaction,
CurrentTransaction,
Transaction,
setActiveInstance,
transaction,
sender,
Expand All @@ -61,7 +59,7 @@ interface TransactionId {
hash(): string | undefined;
}

interface Transaction {
type Transaction = {
/**
* Transaction structure used to describe a state transition on the Mina blockchain.
*/
Expand Down Expand Up @@ -96,7 +94,17 @@ interface Transaction {
* Sends the {@link Transaction} to the network.
*/
send(): Promise<TransactionId>;
}
};

const Transaction = {
fromJSON(json: Types.Json.ZkappCommand): Transaction {
let transaction = ZkappCommand.fromJSON(json);
return newTransaction(
transaction,
(activeInstance as { proofsEnabled?: boolean }).proofsEnabled
);
},
};

type Account = Fetch.Account;

Expand Down Expand Up @@ -270,6 +278,10 @@ function createTransaction(
};

currentTransaction.leave(transactionId);
return newTransaction(transaction, proofsEnabled);
}

function newTransaction(transaction: ZkappCommand, proofsEnabled?: boolean) {
let self: Transaction = {
transaction,
sign(additionalKeys?: PrivateKey[]) {
Expand All @@ -284,7 +296,7 @@ function createTransaction(
return proofs;
},
toJSON() {
let json = zkappCommandToJson(self.transaction);
let json = ZkappCommand.toJSON(self.transaction);
return JSON.stringify(json);
},
toPretty() {
Expand Down Expand Up @@ -357,6 +369,7 @@ function LocalBlockchain({
const actions: Record<string, any> = {};

return {
proofsEnabled,
accountCreationFee: () => UInt64.from(accountCreationFee),
currentSlot() {
return UInt32.from(
Expand Down Expand Up @@ -415,7 +428,7 @@ function LocalBlockchain({
txn.sign();

let commitments = Ledger.transactionCommitments(
JSON.stringify(zkappCommandToJson(txn.transaction))
JSON.stringify(ZkappCommand.toJSON(txn.transaction))
);

if (enforceTransactionLimits)
Expand All @@ -436,7 +449,7 @@ function LocalBlockchain({
}
}

let zkappCommandJson = zkappCommandToJson(txn.transaction);
let zkappCommandJson = ZkappCommand.toJSON(txn.transaction);
try {
ledger.applyJsonTransaction(
JSON.stringify(zkappCommandJson),
Expand Down Expand Up @@ -1206,15 +1219,14 @@ function verifyTransactionLimits(accountUpdates: AccountUpdate[]) {
n2 := signedPair
n1 := signedSingle

10.26*np + 10.08*n2 + 9.14*n1 < 69.45

formula used to calculate how expensive a zkapp transaction is
*/

10.26*np + 10.08*n2 + 9.14*n1 < 69.45
*/
let totalTimeRequired =
proofCost * authTypes['proof'] +
signedPairCost * authTypes['signedPair'] +
signedSingleCost * authTypes['signedSingle'];
proofCost * authTypes.proof +
signedPairCost * authTypes.signedPair +
signedSingleCost * authTypes.signedSingle;

let isWithinCostLimit = totalTimeRequired < costLimit;

Expand Down
3 changes: 2 additions & 1 deletion src/mina-signer/MinaSigner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,8 @@ class Client {
let publicKey = feePayer.feePayer;
let fee = String(fee_);
let nonce = String(feePayer.nonce);
let validUntil = String(feePayer.validUntil ?? defaultValidUntil);
let validUntil =
feePayer.validUntil === undefined ? null : String(feePayer.validUntil);
let memo = Memo.toBase58(Memo.fromString(feePayer.memo ?? ''));
let command: TransactionJson.ZkappCommand = {
feePayer: {
Expand Down
2 changes: 1 addition & 1 deletion src/mina-signer/src/TSTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export type ZkappCommand = {
readonly fee: UInt64;
readonly nonce: UInt32;
readonly memo?: string;
readonly validUntil?: UInt32;
readonly validUntil?: UInt32 | null;
};
};

Expand Down
12 changes: 12 additions & 0 deletions src/mina-signer/src/memo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ function fromString(memo: string) {
'\x00'.repeat(32 - memo.length)
);
}
function toString(memo: string) {
if (memo.length !== 34) {
throw Error(`Memo.toString: length ${memo.length} does not equal 34`);
}
if (memo[0] !== '\x01') {
throw Error('Memo.toString: expected memo to start with 0x01 byte');
}
let length = memo.charCodeAt(1);
if (length > 32) throw Error('Memo.toString: invalid length encoding');
return memo.slice(2, 2 + length);
}

function hash(memo: string) {
let bits = Memo.toBits(memo);
Expand All @@ -43,6 +54,7 @@ const Binable: Binable<string> = defineBinable({

const Memo = {
fromString,
toString,
hash,
...withBits(Binable, SIZE * 8),
...base58(Binable, versionBytes.userCommandMemo),
Expand Down
40 changes: 35 additions & 5 deletions src/mina-signer/src/sign-zkapp-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,23 @@ function signZkappCommand(
networkId: NetworkId
): Json.ZkappCommand {
let zkappCommand = ZkappCommand.fromJSON(zkappCommand_);
let fullCommitment = fullTransactionCommitment(zkappCommand);
let { commitment, fullCommitment } = transactionCommitments(zkappCommand);
let privateKey = PrivateKey.fromBase58(privateKeyBase58);
let publicKey = zkappCommand.feePayer.body.publicKey;

// sign fee payer
let signature = signFieldElement(fullCommitment, privateKey, networkId);
zkappCommand.feePayer.authorization = Signature.toBase58(signature);

// sign other updates with the same public key that require a signature
for (let update of zkappCommand.accountUpdates) {
if (update.body.authorizationKind.isSigned === 0n) continue;
if (!PublicKey.equal(update.body.publicKey, publicKey)) continue;
let { useFullCommitment } = update.body;
let usedCommitment = useFullCommitment === 1n ? fullCommitment : commitment;
let signature = signFieldElement(usedCommitment, privateKey, networkId);
update.authorization = { signature: Signature.toBase58(signature) };
}
return ZkappCommand.toJSON(zkappCommand);
}

Expand All @@ -51,22 +64,39 @@ function verifyZkappCommandSignature(
networkId: NetworkId
) {
let zkappCommand = ZkappCommand.fromJSON(zkappCommand_);
let fullCommitment = fullTransactionCommitment(zkappCommand);
let { commitment, fullCommitment } = transactionCommitments(zkappCommand);
let publicKey = PublicKey.fromBase58(publicKeyBase58);

// verify fee payer signature
let signature = Signature.fromBase58(zkappCommand.feePayer.authorization);
return verifyFieldElement(signature, fullCommitment, publicKey, networkId);
let ok = verifyFieldElement(signature, fullCommitment, publicKey, networkId);
if (!ok) return false;

// verify other signatures for the same public key
for (let update of zkappCommand.accountUpdates) {
if (update.body.authorizationKind.isSigned === 0n) continue;
if (!PublicKey.equal(update.body.publicKey, publicKey)) continue;
let { useFullCommitment } = update.body;
let usedCommitment = useFullCommitment === 1n ? fullCommitment : commitment;
if (update.authorization.signature === undefined) return false;
let signature = Signature.fromBase58(update.authorization.signature);
ok = verifyFieldElement(signature, usedCommitment, publicKey, networkId);
if (!ok) return false;
}
return ok;
}

function fullTransactionCommitment(zkappCommand: ZkappCommand) {
function transactionCommitments(zkappCommand: ZkappCommand) {
let callForest = accountUpdatesToCallForest(zkappCommand.accountUpdates);
let commitment = callForestHash(callForest);
let memoHash = Memo.hash(Memo.fromBase58(zkappCommand.memo));
let feePayerDigest = feePayerHash(zkappCommand.feePayer);
return hashWithPrefix(prefixes.accountUpdateCons, [
let fullCommitment = hashWithPrefix(prefixes.accountUpdateCons, [
memoHash,
feePayerDigest,
commitment,
]);
return { commitment, fullCommitment };
}

type CallTree = { accountUpdate: AccountUpdate; children: CallForest };
Expand Down
3 changes: 3 additions & 0 deletions src/mina-signer/src/signature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ const BinableSignature = withVersionNumber(
const Signature = {
...BinableSignature,
...base58(BinableSignature, versionBytes.signature),
dummy() {
return { r: Field(0), s: Scalar(0) };
},
};

/**
Expand Down
Loading