Verifying signatures and cosigners

Providing multi-signature verification support for dApps

A good example can be found in this repo: off-chain signatures

Signing

All the different Starknet libs give you the ability to sign messages from an account. Let's take starknet.js or starknet-react for example.

Most of the time, you will try to make your users sign a SNIP-12 typed data message.

With starknet.js, you would do something like this:

const account = new Account(provider, address, pk);
const typedData: TypedData = {
    // your typed data
}
const sig = await account.signMessage(typedData);

With starknet-react's hook, it will look like that:

// declare hook
const { signTypedDataAsync } = useSignTypedData({
    // your typedData
})
// use hook
const doSomething = async () => {
    // rest of the code
    const signature = await signTypedDataAsync({
        // your typedData
    })
    // do something with sig
}

You'll notice that a signature is an array of several numbers. If the account is a standard account, then the sig length will be 5 and if it's a smart-account, it will be 9 (because you have both the account and the guardian signatures). The members of the sig array are explained below.

Verifying signatures

There are several ways of verifying a signature, on-chain or off-chain. Most of the time, you will use one of the on-chain methods. The starknet.js doc has an example of off-chain verification.

On-chain verification method 1 - Calling the contract

Argent account signatures can be verified by calling the isValidSignature or is_valid_signature method of the account contract:

const contractAccount = new Contract(abi, accountAddress, provider);
const msgHash = typedData.getMessageHash(data, accountAddress);
await contractAccount.isValidSignature(msgHash, [signature.r, signature.s])

Be aware that most of Ready accounts will return more that one signature that should all be verified. See below.

Signature r and s are the the 4th and 5th member of the sig array so signature[3] and signature[4] .

On-chain verification method 2 - with Typed data

Most of the time, you will get your users to sign a typed data message following the SNIP-12 standard. There is an easy way to verify such a signature.

const provider = new RpcProvider({
  nodeUrl: rpcUrl
});
const isValidSig = await provider.verifyMessageInStarknet(
      typedData, // typed data json your user signed
      signature, // raw user sig, no need to filter r and s
      account // user address
    );

This method also accepts message hash instead of the full json.

Guardians and co-signers

A guardian is a trusted party, added by the user, that acts as a cosigner/co-validator for the user's account when carrying out typical wallet operations or for recovery purposes.

For most of Ready's products e.g Ready, Web Wallet, Smart Accounts in Ready Wallet etc, the guardian is usually Ready's backend.

In the next section, let's take a look at how you can verify multi-signatures for accounts with an active guardian.

Verifying multi-signatures

From a dApp's end, explicit support has to be provided for verifying multi-signatures, or account owners with guardians will be unable to sign transactions.

The signature is verified by calling the isValidSignature or is_valid_signature() method. If the user has a guardian, the signature returned by the wallet will be longer and include more data.

0: number of signers (i.e. 2 in this example)

1: type of signer 1

2: pubkey 1

3: r1

4: s1

5: type of signer 2 (guardian)

6: pubkey 2 (guardian)

7: r2 (guardian)

8: s2 (guardian)

const contractAccount = new Contract(abi, accountAddress, provider);
const msgHash = typedData.getMessageHash(data, accountAddress);
await contractAccount.isValidSignature(msgHash, [signature1.r, signature1.s, signature2.r, signature2.s])

Different Starknet accounts or wallets might return different signatures. Only the signature verification with isValidSignature should be sent as a payload

Last updated

Was this helpful?