A guide on Starknet signatures
Abstract
This article outlines the process of signing and verifying a signature on Starknet. It begins by introducing Account Abstraction and how it modifies signature verification compared to traditional blockchains like Ethereum. It then provides comprehensive code examples in TypeScript and Go for signing a message and verifying a signature using two methods available on Starknet: using the user's public key and using the user's account address.
This post is based on the article originally published on dev.to by Bastien Faivre.
Account Abstraction
Account Abstraction is a core feature of Starknet that fundamentally changes how accounts work compared to chains like Ethereum. In Starknet, accounts are smart contracts that can implement arbitrary logic for transaction validation. This means that different account contracts can implement different signature verification methods. While most Starknet accounts follow a standard for signature verification, it's essential to understand that the account contract itself defines the validation logic.
Signing a message
When signing a message on Starknet, you first need to get a private key and its corresponding account. There are different ways to do so depending on the language and library used. In TypeScript, you can use a library like `starknet` or `starknet.js`. In Go, you can use the `starknet.go` library developed by Nethermind.
Here's an example of signing a message in TypeScript:
import { Signature, constants, ec, hash, stark } from "starknet";
// Generate a private key (usually you would use a secure storage method)
const privateKey = stark.randomAddress();
// Get the public key from the private key
const publicKey = ec.starkCurve.getStarkKey(privateKey);
// The message to sign
const message = "Hello, Starknet!";
// Hash the message
const messageHash = hash.starknetKeccak(message);
// Sign the message hash
const signature = ec.starkCurve.sign(messageHash, privateKey);
// The signature is an array of two elements: r and s
console.log("Signature:", signature);
Verifying a signature using the public key
Verifying a signature using the public key is straightforward on most blockchain platforms. In Starknet, you can verify a signature using the signer's public key with code like this:
// Verify the signature using the public key
const isValid = ec.starkCurve.verify(messageHash, signature, publicKey);
console.log("Is the signature valid?", isValid);
Verifying a signature using the account address
In Starknet, due to Account Abstraction, sometimes you need to verify a signature using the signer's account address rather than their public key. This requires calling the `is_valid_signature` method on the account contract. (Note: The original article mentions `isPrefixedMessageValid`, but standard account contracts often use `is_valid_signature`. The exact method can vary.)
import { Account, Provider, constants, stark, Signature } from "starknet";
// Initialize a provider to connect to Starknet
const provider = new Provider({ sequencer: { network: constants.NetworkName.SN_MAIN } }); // Or your desired network
// Assume signerAddress, pkForSigner, messageHash and signature are defined
// const signerAddress = "0x123...";
// const pkForSigner = "0x456..."; // or connect with a wallet signer
// const messageHash = "0x789...";
// const signature: Signature = ["0xabc...", "0xdef..."];
async function verifySignatureUsingAccount(signerAddress: string, pkForSigner: string, msgHash: string, sig: Signature) {
const account = new Account(provider, signerAddress, pkForSigner);
try {
const result = await account.call("is_valid_signature", [
msgHash, // hash
sig.length.toString(), // signature_len
...sig // signature (r, s)
]);
// For ArgentX/Braavos standard accounts, a single felt "0x1" (VALID_SIGNATURE) is returned on success.
// Cairo0 contracts might return an array if the function has multiple return values.
// Check the specific account contract for exact return format if issues arise.
const isValid = Array.isArray(result) ? result[0] === "0x1" : result.result[0] === "0x1";
console.log("Is the signature valid (using account)?", isValid);
return isValid;
} catch (error) {
console.error("Error verifying signature using account:", error);
return false;
}
}
// Example usage (ensure variables are set)
// verifySignatureUsingAccount(signerAddress, pkForSigner, messageHash, signature);
Working with a wallet (Argent, Braavos, etc.)
When working with a wallet like Argent or Braavos, you typically won't have direct access to the user's private key. Instead, you'll need to use the wallet's interface to request the user to sign a message.
// This is a conceptual example, actual implementation depends on the wallet connector library (e.g., starknet-react, get-starknet)
async function signMessageWithWallet(wallet: any, message: string) { // Use specific wallet type from your library
try {
// The method might be `signMessage`, `personalSign`, or similar
// The exact structure for `typedData` or `message` depends on the wallet and standard (e.g., EIP-712 for Starknet)
const signature = await wallet.account.signMessage({ message }); // Example structure, might need TypedData for some wallets
console.log("Signature from wallet:", signature);
return signature;
} catch (error) {
console.error("Error signing message with wallet:", error);
return null;
}
}
Conclusion
Starknet's Account Abstraction model provides flexibility for signature verification but requires understanding the differences from traditional blockchain signature models. By following the examples provided, you should be able to effectively sign messages and verify signatures in your Starknet applications. Remember that the specific implementation details may vary depending on the wallet provider and account contract being used. Always refer to the specific documentation of the wallet or account contract you're working with.
For more information, check out the original article on dev.to by Bastien Faivre.