Using the SessionKey Validator

The starknet modular account comes with the @0xknwn/starknet-module-sessionkey SDK that you can use to work with the sessionkey validator. In this section, you will see, how to install and use the validator, how to request a sessionkey and how to use it.

Note: This section assumes the SmartrAccount class has been instantiated in the smartrAccount variable as shown in Using the modular account from the SDK. It also assumes the Counter contract that comes with the project has been deploys to the counterAddress and the CounterABI class is available. The 06-setup.ts script that comes with this project ensure those steps are executed.

Install the SessionKey Validator

@0xknwn/starknet-module-sessionkey is a SDK that complements @0xknwn/starknet-modular-account and provide the tools to manage the module and interact with it from the SmartrAccount. Start by adding the module to the project with a command like the one below:

npm install --save @0xknwn/starknet-module-sessionkey

If you are working on a network that does not have the sessionkey module class declared already, like the devnet, you should declare the class. The declareClass function that comes with the SDK allow such an installation. Below is an example of a script that install the module class:

// file src/06-declare-class.ts
import { RpcProvider, Account } from "starknet";
import { declareClass } from "@0xknwn/starknet-module-sessionkey";

// these are the settings for the devnet with --seed=0
// change them to mee your requirements
const providerURL = "http://127.0.0.1:5050/rpc";
const ozAccountAddress =
  "0x64b48806902a367c8598f4f95c305e8c1a1acba5f082d294a43793113115691";
const ozPrivateKey = "0x71d7bb07b9a64f6f78ac4c816aff4da9";

const main = async () => {
  const provider = new RpcProvider({ nodeUrl: providerURL });
  const account = new Account(provider, ozAccountAddress, ozPrivateKey);

  const { classHash: sessionkeyValidatorClassHash } = await declareClass(
    account,
    "SessionKeyValidator"
  );
  console.log("SessionKeyValidator class hash:", sessionkeyValidatorClassHash);
};

main()
  .then(() => {})
  .catch((e) => {
    console.warn(e);
  });

Note: To declare the class, the account you use must be loaded with ETH. In order to do it, you can use any account, including the modular account.

Transpile it and run the script with the commands below:

npx tsc --build

node dist/06-declare-class.js

Note: only the declareClass function from @0xknwn/starknet-module-sessionkey can declare the Sessionkey Validator class. If you are using the declareClass function from @0xknwn/starknet-modular-account, you will be able to declare the Stark Validator and the Modular Account class but not the SessionKey Validator.

Register the SessionKey Validator as a Module

The modular account SDK comes with the addModule, removeModule and isModule. You can use those 3 functions to manage the module in the account once it has been declared to the network. To register the module in the account, use addModule:

// file src/06-add-module.ts
import { SmartrAccount } from "@0xknwn/starknet-modular-account";
import { classHash } from "@0xknwn/starknet-module-sessionkey";
import { init } from "./06-init";
import { RpcProvider } from "starknet";

const providerURL = "http://127.0.0.1:5050/rpc";

const main = async () => {
  const provider = new RpcProvider({ nodeUrl: providerURL });
  const { accountAddress, smartrAccountPrivateKey } = await init();
  const account = new SmartrAccount(
    provider,
    accountAddress,
    smartrAccountPrivateKey
  );
  const { transaction_hash } = await account.addModule(
    classHash("SessionKeyValidator")
  );
  const receipt = await account.waitForTransaction(transaction_hash);
  console.log("transaction succeeded", receipt.isSuccess());

  const isInstalled = await account.isModule(classHash("SessionKeyValidator"));
  console.log(
    "module",
    classHash("SessionKeyValidator"),
    "is installed",
    isInstalled
  );
};

main()
  .then(() => {})
  .catch((e) => {
    console.warn(e);
  });

Transpile and run the script:

npx tsc --build

node dist/06-add-module.js

Requesting a Session Key

Once the module is installed in the account, a dapp can request a grant associated with a private key. In order to get that authorization, it must first build a session key request. A request is made of the following elements:

  • the account address. A session key is granted to an account and cannot be used with another one.
  • the sessionkey validator module class hash. Because an account could have several sessionkey module installed, we want to make sure the session key is granted to a very specific module.
  • the core validator class hash. Here we want to make sure the sessionkey grantor is the core validator for the account to prevent checking a signature with another module from the account
  • the dapp public key that is associated with the session key request. Once the session key is authorized, the dapp will only be able to sign transactions with its private key counterpart.
  • an expiration time. Here again, we want the session key to be limited in time and we strongly suggest the signer keep that period short.
  • an array of policies, i.e a list of (1) contract addresses and (2) entrypoints the dapp will be able to call with that session key. Keep in mind that many contracts are upgradeable and that a good practice would be to grant an access only to a contract that is opensource, audited and not upgradeable.
  • the chain id. This is something that might be remove in a later version of the session key because the account address already depends of the chain id. However, this element is mandatory to request a sessionkey grant

Create and Manage Policies

Policies are very specific to session keys and as such, the sessionkey SDK, i.e @0xknwn/starknet-module-sessionkey provides a class named PolicyManager that helps to manage them. There are 2 main reasons to use that class:

  • When requesting a sessionkey. That is because you do not use the array of policies but a hash of each one of them and a merkle tree of those hashes. The PolicyManager that comes with the SDK provides you with that Merkle tree root that is what is used to request the sessionkey grant.
  • When signing a transaction with the session key. That is because when you sign a transaction, not only you need to provide a valid signature of the transaction with the private key that has been granted the access but you also need to provide the merkle proofs that the call that are used in the transaction are part of the sessionkey policies.

So assuming you want to grant an access to the increment and increment_by entrypoints of the Counter class and assuming counterAddress contains counter address, to use the PolicyManager class, you will call its constructor like below:

const policyManager = new PolicyManager([
  { contractAddress: counterContract.address, selector: "increment" },
  { contractAddress: counterContract.address, selector: "increment_by" },
]);

The policyManager.getRoot() function will return the root of the merkle tree associated with the policies.

Note: The second useful function of PolicyManager is the getProof(policy) function. However, once the session key module registered in SmartrAccount that function is used by the account and you should not use it by yourself.

Create a SessionKeyModule

The SessionKeyModule is an implementation of a module that can be passed to the SmartrAccount and manages the decoration of the transaction under the hood. To fully instantiate that module, you will need:

  • all the data above
  • to request/get an authorization from the core validator signer
  • to add the signature provided by the grantor back into the module

The first step consists in instantiating the SessionKeyModule with all the data associated with the session key:

const sessionKeyModule = new SessionKeyModule(
  sessionkeyPublicKey,
  accountAddress,
  sessionkeyClassHash,
  chain,
  expires,
  policyManager
);

Request a Grant by the Account Core Validator Signer

To request the authorization you should call the request method on the module with the starkValidatorClassHash like below:

const request = await sessionKeyModule.request(starkValidatorClassHash);

Note: this step is very important because it stores the starkValidatorClassHash in the module.

Get the Session Key Grant

Now, you need to use the core validator signer to provide the signature that will be necessary for the module to be activated. The sessionkey SDK provides the SessionKeyGrantor to help you with signing the request. To generate the signature, run the sign method on it:

const grantor = new SessionKeyGrantor(
  starkValidatorClassHash,
  smartrAccountPrivateKey
);
const signature = await grantor.sign(sessionKeyModule);

Register the Core Validator Signature with the SessionKeyModule

To finish with the module configuration, you just have to add the signature to it like below:

sessionKeyModule.add_signature(signature);

Create an Account with the SessionKeyModule

Now you can create an Account with the SmartrAccount class. The class constructor can use a module as a 4th argument of the constructor like below:

const smartrAccountWithSessionKey = new SmartrAccount(
  provider,
  accountAddress,
  sessionkeyPrivateKey,
  sessionKeyModule
);

Note: here you are not using the private key from the core validator signer but the private key generated by your dapp.

Source Code

The overall sessionkey request/grant process with the execution of a transaction on the counter contract is available as a single script below:

// file src/06-sessionkey-transaction.ts
import { SmartrAccount, classHash } from "@0xknwn/starknet-modular-account";
import {
  classHash as sessionkeyClassHash,
  PolicyManager,
  SessionKeyModule,
  SessionKeyGrantor,
} from "@0xknwn/starknet-module-sessionkey";
import { init, CounterABI } from "./06-init";
import { RpcProvider, Signer, Contract } from "starknet";

const providerURL = "http://127.0.0.1:5050/rpc";
const sessionkeyPrivateKey = "0x4";

const main = async () => {
  const { counterAddress, smartrAccountPrivateKey, accountAddress } =
    await init();
  const provider = new RpcProvider({ nodeUrl: providerURL });

  // Step 1: Collect all the necessary information to request a sessionkey
  // Authorization to the modular account signers
  const signer = new Signer(sessionkeyPrivateKey);
  const sessionkeyPublicKey = await signer.getPubKey();

  const chain = await provider.getChainId();

  const expires = BigInt(Math.floor(Date.now() / 1000) + 24 * 60 * 60); // 24 hours from now

  const policyManager = new PolicyManager([
    { contractAddress: counterAddress, selector: "increment" },
    { contractAddress: counterAddress, selector: "increment_by" },
  ]);

  // Step 2: Create a session key module that can be used to request a session
  // key and to create a SmartrAccount with the signed session key that can
  // execute transactions as any regular Account
  const sessionKeyModule = new SessionKeyModule(
    sessionkeyPublicKey,
    accountAddress,
    sessionkeyClassHash("SessionKeyValidator"),
    chain,
    `0x${expires.toString(16)}`,
    policyManager
  );

  // Step 3: Generate the sessionkey grant request
  // that is an important step to request a session key because that is when
  // the core validator class is registered with the session key module
  const request = await sessionKeyModule.request(classHash("StarkValidator"));
  console.log("request", request);

  // Step 4: Use the SessionKeyGrantor helper class to sign the request
  const grantor = new SessionKeyGrantor(
    classHash("StarkValidator"),
    smartrAccountPrivateKey
  );
  const signature = await grantor.sign(sessionKeyModule);

  // Step 5: Register the signatures to the session key module
  sessionKeyModule.add_signature(signature);

  // Step 6: Create the SmartrAccount with the session key module
  const smartrAccountWithSessionKey = new SmartrAccount(
    provider,
    accountAddress,
    sessionkeyPrivateKey,
    sessionKeyModule
  );

  // Step 7: Execute transactions with the session key module
  const counter = new Contract(
    CounterABI,
    counterAddress,
    smartrAccountWithSessionKey
  );
  let currentCounter = await counter.call("get");
  console.log("currentCounter", currentCounter);
  const call = counter.populate("increment");
  const { transaction_hash } = await smartrAccountWithSessionKey.execute(call);
  const receipt = await smartrAccountWithSessionKey.waitForTransaction(
    transaction_hash
  );
  console.log("transaction succeeded", receipt.isSuccess());
  currentCounter = await counter.call("get");
  console.log("currentCounter", currentCounter);
};

main()
  .then(() => {})
  .catch((e) => {
    console.warn(e);
  });

Transpile and run it:

npx tsc --build

node dist/06-sessionkey-transaction.js

Execute a Transaction with the Session Key

As you can see from above, executing a transaction with a session key is exactly the same as executing any transaction with a starknet.js account. All you need to do is to use the SmartrAccount that comes with the @0xknwn/starknet-modular-account SDK with the SessionKeyModule from the @0xknwn/starknet-module-sessionkey SDK.

Invalidate the Session Key

If needed, you can block a session key. To proceed, you would need the hash of the session key as shown in the request. That is the hash that is actually signed by the core validator signer and stored in sessionkeyHash in the script. Then, you can use disable_session_key entrypoint on the module with the executeOnModule entrypoint of the account:

// file src/06-block-sessionkey.ts
import { SmartrAccount } from "@0xknwn/starknet-modular-account";
import {
  classHash as sessionkeyClassHash,
  SessionKeyValidatorABI,
} from "@0xknwn/starknet-module-sessionkey";
import { init } from "./06-init";
import { CallData, RpcProvider } from "starknet";

const providerURL = "http://127.0.0.1:5050/rpc";
const sessionkeyHash = "0x7";

const main = async () => {
  const provider = new RpcProvider({ nodeUrl: providerURL });
  const { accountAddress, smartrAccountPrivateKey } = await init();
  const account = new SmartrAccount(
    provider,
    accountAddress,
    smartrAccountPrivateKey
  );

  const moduleCallData = new CallData(SessionKeyValidatorABI);
  const calldata = moduleCallData.compile("disable_session_key", {
    sessionkey: sessionkeyHash,
  });
  const { transaction_hash } = await account.executeOnModule(
    sessionkeyClassHash("SessionKeyValidator"),
    "disable_session_key",
    calldata
  );
  const receipt = await account.waitForTransaction(transaction_hash);
  console.log("transaction succeeded", receipt.isSuccess());
};

main()
  .then(() => {})
  .catch((e) => {
    console.warn(e);
  });

Transpile and run the script:

npx tsc --build

node dist/06-block-sessionkey.js

Remove the SessionKey Validator Module from the Account

To remove the module from the account, use removeModule:

// file src/06-remove-module.ts
import { SmartrAccount } from "@0xknwn/starknet-modular-account";
import { classHash } from "@0xknwn/starknet-module-sessionkey";
import { init } from "./06-init";
import { RpcProvider } from "starknet";

const providerURL = "http://127.0.0.1:5050/rpc";

const main = async () => {
  const provider = new RpcProvider({ nodeUrl: providerURL });
  const { accountAddress, smartrAccountPrivateKey } = await init();
  const account = new SmartrAccount(
    provider,
    accountAddress,
    smartrAccountPrivateKey
  );
  const { transaction_hash } = await account.removeModule(
    classHash("SessionKeyValidator")
  );
  const receipt = await account.waitForTransaction(transaction_hash);
  console.log("transaction succeeded", receipt.isSuccess());

  const isInstalled = await account.isModule(classHash("SessionKeyValidator"));
  console.log(
    "module",
    classHash("SessionKeyValidator"),
    "has been removed",
    isInstalled
  );
};

main()
  .then(() => {})
  .catch((e) => {
    console.warn(e);
  });

Transpile and run the script with the commands below:

npx tsc --build

node dist/06-remove-module.js