Skip to content

Commit c26c70f

Browse files
cpb8010Copilot
andauthored
feat: update to new session signatures (#235)
* feat: update to new signatures for audited contracts there were some small changes * fix: rust and clippy formats * fix: rebase on main to get latest interfaces * fix: rust tests without session deployment still need e2e fixes * fix: update session tests * fix: address code review comments bigInt to number breaks types * fix: lint err * feat: latest contracts has zksync os support * Update packages/sdk-platforms/rust/zksync-sso-erc4337/crates/zksync-sso-erc4337-core/src/erc4337/account/modular_smart_account/session/create.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: linting --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 58b9186 commit c26c70f

File tree

16 files changed

+321
-110
lines changed

16 files changed

+321
-110
lines changed

examples/demo-app/components/SessionCreator.vue

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,10 @@
7171

7272
<script setup lang="ts">
7373
import { ref, computed } from "vue";
74-
import { createPublicClient, http, parseEther, type Chain, type Address } from "viem";
74+
import { createPublicClient, http, parseEther, type Address, type Chain, encodePacked, keccak256, pad } from "viem";
7575
import { createBundlerClient } from "viem/account-abstraction";
76-
import { createSession, toEcdsaSmartAccount, LimitType } from "zksync-sso-4337/client";
76+
import { privateKeyToAccount } from "viem/accounts";
77+
import { createSession, toEcdsaSmartAccount, LimitType, getSessionHash } from "zksync-sso-4337/client";
7778
7879
interface SessionConfig {
7980
enabled: boolean;
@@ -234,18 +235,36 @@ async function createSessionOnChain() {
234235
235236
// eslint-disable-next-line no-console
236237
console.log("Session spec created:", sessionSpec);
238+
239+
// Generate proof
240+
const sessionHash = getSessionHash(sessionSpec);
241+
242+
// Sign over (sessionHash, account) to bind session to account
243+
// Matches Rust: keccak256([session_hash, account.address().into_word()].concat())
244+
const digest = keccak256(encodePacked(
245+
["bytes32", "bytes32"],
246+
[sessionHash, pad(props.accountAddress as Address)],
247+
));
248+
249+
const sessionSignerAccount = privateKeyToAccount(props.sessionConfig.sessionPrivateKey as `0x${string}`);
250+
const proof = await sessionSignerAccount.sign({
251+
hash: digest,
252+
});
253+
237254
// eslint-disable-next-line no-console
238255
console.log("Calling createSession with:", {
239256
bundlerClient: !!bundlerClient,
240257
sessionSpec: !!sessionSpec,
241258
sessionValidator: props.sessionConfig.validatorAddress,
259+
proof,
242260
});
243261
244262
// Create the session on-chain
245263
let sessionResult;
246264
try {
247265
sessionResult = await createSession(bundlerClient, {
248266
sessionSpec,
267+
proof,
249268
contracts: {
250269
sessionValidator: props.sessionConfig.validatorAddress as Address,
251270
},
@@ -268,6 +287,13 @@ async function createSessionOnChain() {
268287
// eslint-disable-next-line no-console
269288
console.log("Session created successfully! UserOp hash:", userOpHash);
270289
290+
// Wait for the UserOp to be mined
291+
// eslint-disable-next-line no-console
292+
console.log("Waiting for session creation UserOp to be mined...");
293+
await bundlerClient.waitForUserOperationReceipt({ hash: userOpHash });
294+
// eslint-disable-next-line no-console
295+
console.log("Session creation UserOp mined!");
296+
271297
result.value = { userOpHash };
272298
sessionCreated.value = true;
273299
emit("sessionCreated");

packages/sdk-4337/src/abi/SessionKeyValidator.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ export const SessionKeyValidatorAbi = [{
129129
}],
130130
}],
131131
}],
132+
}, {
133+
name: "proof",
134+
type: "bytes",
135+
internalType: "bytes",
132136
}],
133137
outputs: [],
134138
stateMutability: "nonpayable",

packages/sdk-4337/src/client/actions/sessions.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ export type CreateSessionParams = {
2323
*/
2424
sessionSpec: SessionSpec;
2525

26+
/**
27+
* Proof of session key ownership (signature of session hash by session key)
28+
*/
29+
proof: Hex;
30+
2631
/**
2732
* Contract addresses
2833
*/
@@ -82,6 +87,7 @@ export type CreateSessionReturnType = {
8287
* ],
8388
* transferPolicies: [],
8489
* },
90+
* proof: "0x...", // Signature of session hash by session key
8591
* contracts: {
8692
* sessionValidator: "0x...", // SessionKeyValidator module address
8793
* },
@@ -96,15 +102,15 @@ export async function createSession<
96102
client: Client<TTransport, TChain, TAccount>,
97103
params: CreateSessionParams,
98104
): Promise<CreateSessionReturnType> {
99-
const { sessionSpec, contracts } = params;
105+
const { sessionSpec, proof, contracts } = params;
100106

101107
if (!client.account) {
102108
throw new Error("Client must have an account");
103109
}
104110

105111
// Convert SessionSpec into JSON string expected by wasm helper
106112
const sessionSpecJSON = sessionSpecToJSON(sessionSpec);
107-
const callData = encode_create_session_call_data(sessionSpecJSON) as Hex;
113+
const callData = encode_create_session_call_data(sessionSpecJSON, proof) as Hex;
108114
// Send the UserOperation to create the session using the bundler client method.
109115
// Note: We explicitly pass the account for broader viem compatibility.
110116
const bundler = client as unknown as {

packages/sdk-4337/src/client/passkey/client-actions.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export type PasskeyClientActions<TChain extends Chain = Chain, TAccount extends
5050
/**
5151
* Create a session on-chain using the provided specification
5252
*/
53-
createSession: (params: { sessionSpec: SessionSpec; contracts: { sessionValidator: Address } }) => Promise<Hash>;
53+
createSession: (params: { sessionSpec: SessionSpec; proof: Hex; contracts: { sessionValidator: Address } }) => Promise<Hash>;
5454
};
5555

5656
/**
@@ -93,11 +93,11 @@ export function passkeyClientActions<
9393
},
9494

9595
// Create a session on-chain using the provided specification
96-
createSession: async (params: { sessionSpec: SessionSpec; contracts: { sessionValidator: Address } }) => {
96+
createSession: async (params: { sessionSpec: SessionSpec; proof: Hex; contracts: { sessionValidator: Address } }) => {
9797
// Build smart account instance (lazy, not cached here; acceptable overhead for now)
9898
const smartAccount = await toPasskeySmartAccount(config.passkeyAccount);
9999
const sessionSpecJSON = sessionSpecToJSON(params.sessionSpec);
100-
const callData = encode_create_session_call_data(sessionSpecJSON) as unknown as `0x${string}`;
100+
const callData = encode_create_session_call_data(sessionSpecJSON, params.proof) as unknown as `0x${string}`;
101101
const userOpHash = await config.bundler.sendUserOperation({
102102
account: smartAccount,
103103
calls: [

packages/sdk-4337/src/client/session/session.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ describe("Session Utils", () => {
116116
const parsed = JSON.parse(json);
117117
expect(parsed.signer).toBe(signer);
118118
expect(typeof parsed.expiresAt).toBe("string"); // bigints as strings
119-
expect(parsed.feeLimit.limitType).toBe(0); // LimitType.Unlimited = 0
119+
expect(parsed.feeLimit.limitType).toBe("Unlimited"); // LimitType.Unlimited
120120
});
121121

122122
it("should handle complex session spec with policies", () => {
@@ -148,7 +148,7 @@ describe("Session Utils", () => {
148148

149149
expect(parsed.callPolicies).toHaveLength(1);
150150
expect(parsed.transferPolicies).toHaveLength(1);
151-
expect(parsed.feeLimit.limitType).toBe(1); // LimitType.Lifetime = 1
151+
expect(parsed.feeLimit.limitType).toBe("Lifetime"); // LimitType.Lifetime
152152
expect(parsed.feeLimit.limit).toBe("1000000000000000000");
153153
});
154154
});

packages/sdk-4337/src/client/session/types.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import type { Address, Hex } from "viem";
22

33
/**
44
* Limit types for usage tracking
5-
* Uses string values to match Rust serialization format
5+
* Uses numeric values to match contract enum
66
*/
77
export enum LimitType {
8-
Unlimited = "Unlimited",
9-
Lifetime = "Lifetime",
10-
Allowance = "Allowance",
8+
Unlimited = 0,
9+
Lifetime = 1,
10+
Allowance = 2,
1111
}
1212

1313
/**

packages/sdk-4337/src/client/session/utils.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import type { Address, Hex } from "viem";
1+
import { type Address, encodeAbiParameters, type Hex, keccak256 } from "viem";
22

3-
import type { SessionSpec, UsageLimit } from "./types.js";
3+
import { SessionKeyValidatorAbi } from "../../abi/SessionKeyValidator.js";
4+
import { LimitType, type SessionSpec, type UsageLimit } from "./types.js";
45

56
/**
67
* Utility type that converts all bigint values to strings recursively
@@ -23,11 +24,17 @@ export type SessionSpecJSON = ConvertBigIntToString<SessionSpec>;
2324
* All bigint values are converted to strings for safe serialization.
2425
*/
2526
export function sessionSpecToJSON(spec: SessionSpec): string {
26-
const usageLimitToJSON = (limit: UsageLimit) => ({
27-
limitType: limit.limitType,
28-
limit: limit.limit.toString(),
29-
period: limit.period.toString(),
30-
});
27+
const usageLimitToJSON = (limit: UsageLimit) => {
28+
let limitType = "Unlimited";
29+
if (limit.limitType === LimitType.Lifetime) limitType = "Lifetime";
30+
else if (limit.limitType === LimitType.Allowance) limitType = "Allowance";
31+
32+
return {
33+
limitType,
34+
limit: limit.limit.toString(),
35+
period: limit.period.toString(),
36+
};
37+
};
3138

3239
return JSON.stringify({
3340
signer: spec.signer,
@@ -130,3 +137,24 @@ export function isSessionExpired(
130137
export function getSessionExpiryDate(spec: SessionSpec): Date {
131138
return new Date(Number(spec.expiresAt) * 1000);
132139
}
140+
141+
/**
142+
* Computes the hash of a session specification.
143+
* This hash is signed by the session key to prove ownership.
144+
*/
145+
export function getSessionHash(spec: SessionSpec): Hex {
146+
const createSessionFunction = SessionKeyValidatorAbi.find(
147+
(x) => x.type === "function" && x.name === "createSession",
148+
);
149+
if (!createSessionFunction) throw new Error("createSession function not found in SessionKeyValidator ABI");
150+
151+
const sessionSpecParam = createSessionFunction.inputs.find((x) => x.name === "sessionSpec");
152+
if (!sessionSpecParam) throw new Error("sessionSpec parameter not found in createSession function inputs");
153+
154+
const encoded = encodeAbiParameters(
155+
[sessionSpecParam],
156+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
157+
[spec as any],
158+
);
159+
return keccak256(encoded);
160+
}

packages/sdk-platforms/rust/zksync-sso-erc4337/crates/zksync-sso-erc4337-core/src/erc4337/account/modular_smart_account/deploy.rs

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ mod tests {
218218
calls::{Execution, encode_calls},
219219
module::{
220220
Module,
221+
add::{AddModuleParams, AddModulePayload, add_module},
221222
installed::{
222223
IsModuleInstalledParams, is_module_installed,
223224
},
@@ -246,8 +247,9 @@ mod tests {
246247
},
247248
};
248249
use alloy::{
249-
primitives::{U256, Uint, address},
250-
signers::local::PrivateKeySigner,
250+
primitives::{U256, Uint, address, keccak256},
251+
signers::{SignerSync, local::PrivateKeySigner},
252+
sol_types::SolValue,
251253
};
252254
use std::{future::Future, pin::Pin, str::FromStr, sync::Arc};
253255

@@ -458,30 +460,27 @@ mod tests {
458460
PrivateKeySigner::from_str(session_key_hex)?.address();
459461
println!("Session signer address: {}", session_signer_address);
460462

461-
// Deploy account WITH session validator pre-installed
463+
// Deploy account WITHOUT session validator pre-installed
462464
let signers =
463465
vec![address!("0xa0Ee7A142d267C1f36714E4a8F75612F20a79720")];
464466
let eoa_signers = EOASigners {
465467
addresses: signers,
466468
validator_address: eoa_validator_address,
467469
};
468-
let session_config = SessionValidatorConfig {
469-
validator_address: session_validator_address,
470-
};
471470

472471
let address = deploy_account(DeployAccountParams {
473472
factory_address,
474473
eoa_signers: Some(eoa_signers),
475474
webauthn_signer: None,
476-
session_validator: Some(session_config),
475+
session_validator: None,
477476
id: None,
478477
provider: provider.clone(),
479478
})
480479
.await?;
481480

482-
println!("✓ Account deployed with session validator pre-installed");
481+
println!("✓ Account deployed (without session validator)");
483482

484-
// Verify both modules are installed
483+
// Verify EOA module is installed
485484
let is_eoa_installed = is_module_installed(IsModuleInstalledParams {
486485
module: Module::eoa_validator(eoa_validator_address),
487486
account: address,
@@ -490,6 +489,31 @@ mod tests {
490489
.await?;
491490
eyre::ensure!(is_eoa_installed, "EOA validator not installed");
492491

492+
// Fund the account
493+
fund_account_with_default_amount(address, provider.clone()).await?;
494+
println!("✓ Account funded");
495+
496+
// Create EOA signer
497+
let eoa_signer = create_eoa_signer(
498+
signer_private_key.clone(),
499+
eoa_validator_address,
500+
)?;
501+
502+
// Install Session Validator Module
503+
add_module(AddModuleParams {
504+
account_address: address,
505+
module: AddModulePayload::session_key(session_validator_address),
506+
entry_point_address,
507+
paymaster: None,
508+
provider: provider.clone(),
509+
bundler_client: bundler_client.clone(),
510+
signer: eoa_signer.clone(),
511+
})
512+
.await?;
513+
514+
println!("✓ Session validator installed");
515+
516+
// Verify session module is installed
493517
let is_session_installed =
494518
is_module_installed(IsModuleInstalledParams {
495519
module: Module::session_key_validator(
@@ -503,16 +527,6 @@ mod tests {
503527

504528
println!("✓ Both EOA and Session validators verified as installed");
505529

506-
// Fund the account
507-
fund_account_with_default_amount(address, provider.clone()).await?;
508-
println!("✓ Account funded");
509-
510-
// Create a session using EOA signer
511-
let eoa_signer = create_eoa_signer(
512-
signer_private_key.clone(),
513-
eoa_validator_address,
514-
)?;
515-
516530
let expires_at = Uint::from(2088558400u64); // Year 2036
517531
let target = address!("0xa0Ee7A142d267C1f36714E4a8F75612F20a79720");
518532

@@ -536,6 +550,16 @@ mod tests {
536550
}],
537551
};
538552

553+
// Calculate proof
554+
let session_lib_spec: crate::erc4337::account::modular_smart_account::session::contract::SessionLib::SessionSpec = session_spec.clone().into();
555+
let session_hash = keccak256(session_lib_spec.abi_encode());
556+
let digest = keccak256((session_hash, address).abi_encode());
557+
558+
let session_signer_instance =
559+
PrivateKeySigner::from_str(session_key_hex)?;
560+
let proof =
561+
session_signer_instance.sign_hash_sync(&digest)?.as_bytes().into();
562+
539563
create_session(CreateSessionParams {
540564
account_address: address,
541565
spec: session_spec.clone(),
@@ -545,6 +569,7 @@ mod tests {
545569
bundler_client: bundler_client.clone(),
546570
provider: provider.clone(),
547571
signer: eoa_signer,
572+
proof,
548573
})
549574
.await?;
550575

0 commit comments

Comments
 (0)