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 Argent's 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 Argent's products e.g Argent Mobile, Web Wallet, Smart Accounts in Argent X etc, the guardian is usually Argent'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?