Using the P256 Validator

The P256 Validator Module can both work as a Secondary or as the Core Validator for the account. It requires the module SDK. In this section of the documentation, you will see how you can use the Modular Account to interact with the P256 Validator Module.

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 05-setup.ts script that comes with this project ensure those steps are executed.

Installing the P256 Validator SDK

If you plan to use the P256 Validatoi module, you might need the @0xknwn/starknet-module SDK in addition to the @0xknwn/starknet-modular-account SDK. To install it, run:

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

Declaring the P256 Validator

If you are working on a network that does not have the P256 validator class already declared, you will need to declare it. The P256 validator module SDK, aka @0xknwn/starknet-module contains a helper function named declareClass to declare the class to the network. To use it, you need to pass:

  • A starknet.js Account as a first parameter
  • The name of the class to declare as the 2nd parameter. For the P256 Validator, the name isP256Validator

Below is an example of a script that declares the new classes.

// file src/05-declare-p256-validator.ts
import { RpcProvider, Account } from "starknet";
import { declareClass } from "@0xknwn/starknet-module";

// these are the settings for the devnet with --seed=0
// change them to mee your requirements
const ozAccountAddress =
  "0x3b2d6d0edcbdbdf6548d2b79531263628887454a0a608762c71056172d36240";
const ozPrivateKey =
  "0x000e8f079f1092042bf9b855935d3ef1bb7078609491fb24e7cb8cbb574e50ca";
const providerURL = "https://starknet-sepolia.public.blastapi.io";

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

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

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

Note: To declare the class, the account you use must be loaded with P256.

Assuming you have named the script src/05-declare-p256-validator.ts, transpile and run it:

npx tsc --build

node dist/05-declare-p256-validator.js

The output should return the hash for the class.

Verify the P256 Validator class hash

The class hash does NOT depend on the deployment or the network. So you can find them at any time with the classHash helper that comes with the SDK. The script below shows how to use that function:

// file src/05-check-p256-validator.ts
import { classHash } from "@0xknwn/starknet-module";

console.log("Computed P256Validator class hash:", classHash("P256Validator"));

Transpile and run the script:

npx tsc --build

node dist/05-check-p256-validator.js

Using the P256 Validator as a Secondary Validator

The simplest way to use the P256 Validator is to add it as a module to an existing account and execute a transaction with the P256Module class from the @0xknwn/starknet-module.

Register the P256 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/05-add-module.ts
import { SmartrAccount } from "@0xknwn/starknet-modular-account";
import { classHash } from "@0xknwn/starknet-module";
import { init } from "./05-init";
import { RpcProvider } from "starknet";

const ozAccountAddress =
  "0x3b2d6d0edcbdbdf6548d2b79531263628887454a0a608762c71056172d36240";
const ozPrivateKey =
  "0x000e8f079f1092042bf9b855935d3ef1bb7078609491fb24e7cb8cbb574e50ca";
const providerURL = "https://starknet-sepolia.public.blastapi.io";

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("P256Validator")
  );
  const receipt = await account.waitForTransaction(transaction_hash);
  console.log("transaction succeeded", receipt.isSuccess());

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

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

Transpile and run the script:

npx tsc --build

node dist/05-add-module.js

Register the public key associated with your P256 Private Key

Every module comes with a set of Management API. In the case of the P256 Validator, the associated interfaces are the following:

#![allow(unused)]
fn main() {
#[starknet::interface]
pub trait IPublicKey<TState> {
    fn set_public_key(ref self: TState, new_public_key: P256PublicKey);
    fn get_public_key(self: @TState) -> P256PublicKey;
}
}

Now that you have installed the module, you can create an ETH private key and register the associated public key in the module. For the purpose of the demonstration, we will use an arbitrary (and now unsafe) private/public key pair:

  • private key: 0x1efecf7ee1e25bb87098baf2aaab0406167aae0d5ea9ba0d31404bf01886bd0e
  • public key:
    • x: 0x097420e05fbc83afe4d73b31890187d0cacf2c3653e27f434701a91625f916c2
      • x.low: 269579757328574126121444003492591638210
      • x.high: 12566025211498978771503502663570524112
    • y: 0x98a304ff544db99c864308a9b3432324adc6c792181bae33fe7a4cbd48cf263a
      • y.low: 230988565823064299531546210785320445498
      • y.high: 202889101106158949967186230758848275236

Because Starknet types can only manage felt252 that are smaller than uint256 the format used by P256PublicKey is actually an array<felt252> that is made of [x.low, x.high, y.low, y.high]. To register the public key, use the script below:

// file src/05-register-publickey.ts
import { SmartrAccount } from "@0xknwn/starknet-modular-account";
import {
  classHash as moduleClassHash,
  P256Signer,
} from "@0xknwn/starknet-module";
import { cairo } from "starknet";
import { init } from "./05-init";
import { RpcProvider } from "starknet";

const providerURL = "https://starknet-sepolia.public.blastapi.io";
const p256PrivateKey =
  "0x1efecf7ee1e25bb87098baf2aaab0406167aae0d5ea9ba0d31404bf01886bd0e";

const main = async () => {
  const provider = new RpcProvider({ nodeUrl: providerURL });
  const { accountAddress, smartrAccountPrivateKey } = await init();
  const account = new SmartrAccount(
    provider,
    accountAddress,
    smartrAccountPrivateKey
  );
  const module_class_hash = moduleClassHash("P256Validator");
  const signer = new P256Signer(p256PrivateKey);
  const publicKey = await signer.getPubKey();
  const coords = publicKey.slice(2, publicKey.length);
  const x = coords.slice(0, 64);
  const x_felts = cairo.uint256(`0x${x}`);
  const y = coords.slice(64, 128);
  const y_felts = cairo.uint256(`0x${y}`);
  console.log("x:", `0x${x}`);
  console.log("(x.low:", x_felts.low, ", x.high:", x_felts.high, ")");
  console.log("y:", `0x${y}`);
  console.log("(y.low:", y_felts.low, ", y.high:", y_felts.high, ")");
  const { transaction_hash } = await account.executeOnModule(
    module_class_hash,
    "set_public_key",
    [
      x_felts.low.toString(),
      x_felts.high.toString(),
      y_felts.low.toString(),
      y_felts.high.toString(),
    ]
  );
  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/05-register-publickey.js

You can check the public key is correctly registered with the script below:

// file src/05-get-publickey.ts
import { SmartrAccount } from "@0xknwn/starknet-modular-account";
import {
  classHash as moduleClassHash,
  P256ValidatorABI,
} from "@0xknwn/starknet-module";
import { init } from "./05-init";
import { CallData, RpcProvider } from "starknet";

const providerURL = "https://starknet-sepolia.public.blastapi.io";

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(P256ValidatorABI);
  const calldata = moduleCallData.compile("get_public_key", {});
  const public_keys = await account.callOnModule(
    moduleClassHash("P256Validator"),
    "get_public_key",
    calldata
  );
  public_keys.forEach((public_key, idx) =>
    console.log(`public key (${idx}):`, public_key)
  );
};

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

Transpile and run the script:

npx tsc --build

node dist/05-get-publickey.js

Run a transaction with the P256Module

The P256Module 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:

  • to instantiate the P256Module module from the SDK
  • to use the P256Signer provided by `@0xknwn/starknet-module with the Private Key
  • to instantiate the SmartrAccount with the 2 classes above

Then you can run a transaction, exactly as you would do with any Starknet.js account. The example below execute the increment entrypoint on the Counter contract:

// file src/05-execute-tx.ts
import { SmartrAccount } from "@0xknwn/starknet-modular-account";
import { init, CounterABI } from "./05-init";
import { RpcProvider, Contract } from "starknet";
import { P256Module, P256Signer } from "@0xknwn/starknet-module";

const providerURL = "https://starknet-sepolia.public.blastapi.io";
const p256PrivateKey =
  "0x1efecf7ee1e25bb87098baf2aaab0406167aae0d5ea9ba0d31404bf01886bd0e";
const counterAddress =
  "0x31c527e5bfe99c50aaa7573b383d298aa9ca70f96ab3834b448e2ba7ee651c1";

const main = async () => {
  const provider = new RpcProvider({ nodeUrl: providerURL });
  const { accountAddress, smartrAccountPrivateKey } = await init();
  console.log("accountAddress", accountAddress);
  const signer = new P256Signer(p256PrivateKey);
  const p256Module = new P256Module(accountAddress);
  const account = new SmartrAccount(
    provider,
    accountAddress,
    signer,
    p256Module
  );
  console.log("counterAddress", counterAddress);
  const counter = new Contract(CounterABI, counterAddress, account);
  let currentCounter = await counter.call("get");
  console.log("currentCounter", currentCounter);
  const call = counter.populate("increment");
  const { transaction_hash } = await account.execute(call);
  console.log("transaction_hash", transaction_hash);
  const receipt = await account.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 the script:

npx tsc --build

node dist/05-execute-tx.js

Remove the P256 Validator Module

You can use removeModule and isModule to remove the module from the account with the script below:

// file src/05-remove-module.ts
import { SmartrAccount } from "@0xknwn/starknet-modular-account";
import { classHash } from "@0xknwn/starknet-module";
import { init } from "./05-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("P256Validator")
  );
  const receipt = await account.waitForTransaction(transaction_hash);
  console.log("transaction succeeded", receipt.isSuccess());

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

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

Transpile and run the script:

npx tsc --build

node dist/05-remove-module.js

Using the P256 Validator as the Core Validator

You can also use the P256 Validator as a Core Validator for the account. For that purpose you will deploy a new account and use the P256Signer to validate the account deployment without registering the P256Module in the SmartrAccount. In order to proceed you need to:

  • Generate the public key as an Array of felt252
  • Compute the account address
  • Send ETH to the modular account address
  • Deploy the Account

Compute the Public Key as an Array of felt252

The P256Signer helps to generate the public key from the private key. Once you have the public key you should slice to get an array of 4 pieces like below:

const p236SmartrSigner = new P256Signer(p256PrivateKey);
const publicKey = await p236SmartrSigner.getPubKey();
const coords = publicKey.slice(2, publicKey.length);
const x = coords.slice(0, 64);
const x_felts = cairo.uint256(`0x${x}`);
const y = coords.slice(64, 128);
const y_felts = cairo.uint256(`0x${y}`);
const publicKeyArray = [
  x_felts.low.toString(),
  x_felts.high.toString(),
  y_felts.low.toString(),
  y_felts.high.toString(),
];

Compute the Account Address

Once you have the public key, you should use the accountAddress function from @0xknwn/starknet-modular-account to compute the address of the account you will install. As a Salt, we will use the hash.computeHashOnElements from the public key like below:

const publicKeyHash = hash.computeHashOnElements(publicKeyArray);
const computedAccountAddress = accountAddress(
  "SmartrAccount",
  publicKeyHash,
  [moduleClassHash("P256Validator"), "0x4", ...publicKeyArray]
);

Note: The "0x4" that is inserted in the calldata is here to indicate there are 4 pieces to the public key:

Send ETH to the SmartrAccount Address to deploy it

To deploy the account, you need to have ETH associated with the target account address. Assuming you have access to an account with ETH, this is how you send p256 to the computedAccountAddress:

const account = new SmartrAccount(
  provider,
  ozAccountAddress,
  smartrAccountPrivateKey
);
const ETH = new Contract(ERC20ABI, ethAddress, account);
const initial_EthTransfer = cairo.uint256(5n * 10n ** 15n);
const call = ETH.populate("transfer", {
  recipient: computedAccountAddress,
  amount: initial_EthTransfer,
});
const { transaction_hash } = await account.execute(call);
const output = await account.waitForTransaction(transaction_hash);

Deploy the Account with the P256 Validator as Core

To deploy the account, you will need to use the deployAccount helper function from @0xknwn/starknet-modular-account with a SmartrAccount that has been instantiated with a P256Signer like below:

const p256Account = new SmartrAccount(
  provider,
  computedAccountAddress,
  p236SmartrSigner
);
const address = await deployAccount(
  p256Account,
  "SmartrAccount",
  publicKeyHash,
  [moduleClassHash("P256Validator"), "0x4", ...publicKeyArray]
);

The Script Code

You will find below the whole script that does the account deployment:

// file src/05-deploy-account.ts
import { RpcProvider, Contract, cairo, hash } from "starknet";
import {
  accountAddress,
  deployAccount,
  SmartrAccount,
} from "@0xknwn/starknet-modular-account";
import {
  classHash as moduleClassHash,
  P256Signer,
} from "@0xknwn/starknet-module";
import { init } from "./05-init";
import { ABI as ERC20ABI } from "./abi/ERC20";
const ethAddress =
  "0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7";

// 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 p256PrivateKey =
  "0x1efecf7ee1e25bb87098baf2aaab0406167aae0d5ea9ba0d31404bf01886bd0e";

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

  // Step 1 - Get the public key from the Eth Signer
  const p236SmartrSigner = new P256Signer(p256PrivateKey);
  const publicKey = await p236SmartrSigner.getPubKey();
  const coords = publicKey.slice(2, publicKey.length);
  const x = coords.slice(0, 64);
  const x_felts = cairo.uint256(`0x${x}`);
  const y = coords.slice(64, 128);
  const y_felts = cairo.uint256(`0x${y}`);
  const publicKeyArray = [
    x_felts.low.toString(),
    x_felts.high.toString(),
    y_felts.low.toString(),
    y_felts.high.toString(),
  ];

  // Step 2 - Compute the account address
  const publicKeyHash = hash.computeHashOnElements(publicKeyArray);
  const computedAccountAddress = accountAddress(
    "SmartrAccount",
    publicKeyHash,
    [moduleClassHash("P256Validator"), "0x4", ...publicKeyArray]
  );

  // Step 3 - Send ETH to the computed account address
  const account = new SmartrAccount(
    provider,
    ozAccountAddress,
    smartrAccountPrivateKey
  );
  const ETH = new Contract(ERC20ABI, ethAddress, account);
  const initial_EthTransfer = cairo.uint256(5n * 10n ** 15n);
  const call = ETH.populate("transfer", {
    recipient: computedAccountAddress,
    amount: initial_EthTransfer,
  });
  const { transaction_hash } = await account.execute(call);
  const output = await account.waitForTransaction(transaction_hash);
  if (!output.isSuccess()) {
    throw new Error("Could not send ETH to the expected address");
  }

  // Step 4 - Deploy the account with the P256Validator as Core Validator
  const p256Account = new SmartrAccount(
    provider,
    computedAccountAddress,
    p236SmartrSigner
  );
  const address = await deployAccount(
    p256Account,
    "SmartrAccount",
    publicKeyHash,
    [moduleClassHash("P256Validator"), "0x4", ...publicKeyArray]
  );
  if (address !== computedAccountAddress) {
    throw new Error(
      `The account should have been deployed to ${computedAccountAddress}, instead ${address}`
    );
  }
  console.log("accountAddress", computedAccountAddress);
  console.log("public key", publicKeyArray);
};

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

Transpile and run it:

npx tsc --build

node dist/05-deploy-account.js

Running a transaction with the P256 Validator as Core

Running a transaction with the P256Validator as a Core is no more complex than running a transaction on a regular account. All you need to do is

  • get the account address that could have been saved from earlier
  • instantiate the SmartrAccount with the Starknet.js P256Signer
  • execute the transaction

Below is an example that assumes you have deployed the account with the 05-deploy-account.ts script earlier:

// file src/05-execute-tx-core.ts
import {
  SmartrAccount,
  accountAddress,
} from "@0xknwn/starknet-modular-account";
import { init, CounterABI } from "./05-init";
import { RpcProvider, Contract, cairo, hash } from "starknet";
import {
  classHash as moduleClassHash,
  P256Signer,
} from "@0xknwn/starknet-module";
const providerURL = "http://127.0.0.1:5050/rpc";
const p256PrivateKey =
  "0x1efecf7ee1e25bb87098baf2aaab0406167aae0d5ea9ba0d31404bf01886bd0e";

const main = async () => {
  // recompute the account address
  const signer = new P256Signer(p256PrivateKey);
  const publicKey = await signer.getPubKey();
  const coords = publicKey.slice(2, publicKey.length);
  const x = coords.slice(0, 64);
  const x_felts = cairo.uint256(`0x${x}`);
  const y = coords.slice(64, 128);
  const y_felts = cairo.uint256(`0x${y}`);
  const publicKeyArray = [
    x_felts.low.toString(),
    x_felts.high.toString(),
    y_felts.low.toString(),
    y_felts.high.toString(),
  ];

  const publicKeyHash = hash.computeHashOnElements(publicKeyArray);
  const computedAccountAddress = accountAddress(
    "SmartrAccount",
    publicKeyHash,
    [moduleClassHash("P256Validator"), "0x4", ...publicKeyArray]
  );

  // execute the transaction
  const provider = new RpcProvider({ nodeUrl: providerURL });
  const { counterAddress } = await init();
  const account = new SmartrAccount(provider, computedAccountAddress, signer);
  const counter = new Contract(CounterABI, counterAddress, account);
  let currentCounter = await counter.call("get");
  console.log("currentCounter", currentCounter);
  const call = counter.populate("increment");
  const { transaction_hash } = await account.execute(call);
  const receipt = await account.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/05-execute-tx-core.js

As you can see from the script:

  • You do not need the P256Module to interact with the account. That is because the validator is used as a Core Validator and, as such, the transaction does not required to be prefixed
  • Running transactions is the same as running a transaction with the Stark validator. Only the signature changes!