Documentation Index
Fetch the complete documentation index at: https://voltaire.tevm.sh/llms.txt
Use this file to discover all available pages before exploring further.
Try it Live
Run Authorization examples in the interactive playground
Signing & Verification
Authorization hashing, signing, and signature verification.
hash
Calculate signing hash for unsigned authorization.
Formula: keccak256(MAGIC_BYTE || rlp([chainId, address, nonce]))
Where:
MAGIC_BYTE = 0x05 (EIP-7702 identifier)
- RLP encoding uses compact representation (no leading zeros)
Namespace API
Factory API
Authorization.hash(unsigned: Authorization.Unsigned): Hash
Parameters:
unsigned: Unsigned authorization to hash
Returns: 32-byte hash to signExample:import { Authorization } from 'tevm';
const unsigned: Authorization.Unsigned = {
chainId: 1n,
address: contractAddress,
nonce: 0n
};
const sigHash = Authorization.hash(unsigned);
console.log(`Hash to sign: ${sigHash}`);
Authorization.Hash({ keccak256, rlpEncode })(unsigned: Authorization.Unsigned): Hash
Tree-shakeable factory with explicit crypto dependencies.Dependencies:
keccak256: (data: Uint8Array) => Uint8Array - Keccak256 hash function
rlpEncode: (data: Array<Uint8Array>) => Uint8Array - RLP encode function
Example:import { Authorization } from 'tevm';
import { hash as keccak256 } from 'tevm/crypto/Keccak256';
import { encode as rlpEncode } from 'tevm/Rlp';
const hash = Authorization.Hash({ keccak256, rlpEncode });
const unsigned: Authorization.Unsigned = {
chainId: 1n,
address: contractAddress,
nonce: 0n
};
const sigHash = hash(unsigned);
Bundle size: Crypto only included if you import it.
Implementation Details
RLP Encoding:
- Encode chainId as compact bigint (remove leading zeros)
- Encode address as 20-byte array
- Encode nonce as compact bigint
- Wrap in RLP list structure
Hashing:
- Prepend MAGIC_BYTE (0x05)
- Apply Keccak-256
Example:
unsigned = { chainId: 1, address: 0x742d...bEb2, nonce: 0 }
RLP([1, 0x742d...bEb2, 0]) = 0xe594742d35cc6634c0532925a3b844bc9e7595f0beb280
MAGIC || RLP = 0x05e594742d35cc6634c0532925a3b844bc9e7595f0beb280
hash = keccak256(0x05e594...) = 0x123...def
Why MAGIC_BYTE?
EIP-7702 uses 0x05 to:
- Prevent cross-protocol replay attacks
- Distinguish from other signing formats (EIP-191, EIP-712)
- Ensure unique hash domain
sign
Create signed authorization from unsigned authorization.
Process:
- Hash unsigned authorization
- Sign hash with secp256k1
- Recover yParity by attempting recovery
- Return Authorization.Item with signature
Namespace API
Factory API
Authorization.sign(
unsigned: Authorization.Unsigned,
privateKey: Uint8Array
): Authorization.Item
Parameters:
unsigned: Authorization to sign
privateKey: 32-byte secp256k1 private key
Returns: Signed Authorization.ItemExample:import { Authorization } from 'tevm';
const unsigned: Authorization.Unsigned = {
chainId: 1n,
address: contractAddress,
nonce: 0n
};
const privateKey = Bytes32();
// ... fill with your private key
const auth = Authorization.sign(unsigned, privateKey);
console.log(`Signature:`);
console.log(` r: ${auth.r}`);
console.log(` s: ${auth.s}`);
console.log(` v: ${auth.yParity}`);
Authorization.Sign({
keccak256,
rlpEncode,
sign,
recoverPublicKey,
addressFromPublicKey
})(unsigned: Authorization.Unsigned, privateKey: Uint8Array): Authorization.Item
Tree-shakeable factory with explicit crypto dependencies.Dependencies:
keccak256: (data: Uint8Array) => Uint8Array - Keccak256 hash function
rlpEncode: (data: Array<Uint8Array>) => Uint8Array - RLP encode function
sign: (messageHash: Uint8Array, privateKey: Uint8Array) => {r, s, v} - secp256k1 sign
recoverPublicKey: (signature, messageHash) => Uint8Array - secp256k1 recovery
addressFromPublicKey: (x: bigint, y: bigint) => Address - Address derivation
Example:import { Authorization } from 'tevm';
import { hash as keccak256 } from 'tevm/crypto/Keccak256';
import { encode as rlpEncode } from 'tevm/Rlp';
import { sign as secp256k1Sign } from 'tevm/crypto/Secp256k1';
import { recoverPublicKey } from 'tevm/crypto/Secp256k1';
import { fromPublicKey as addressFromPublicKey } from 'tevm/Address';
const sign = Authorization.Sign({
keccak256,
rlpEncode,
sign: secp256k1Sign,
recoverPublicKey,
addressFromPublicKey
});
const unsigned: Authorization.Unsigned = {
chainId: 1n,
address: contractAddress,
nonce: 0n
};
const privateKey = Bytes32();
// ... your private key
const auth = sign(unsigned, privateKey);
Bundle size: Only includes imported crypto dependencies.
Implementation Details
Signing Process:
-
Hash Authorization
const messageHash = hash(unsigned);
-
Sign with secp256k1
const sig = Secp256k1.sign(messageHash, privateKey);
// Returns { r: Uint8Array(32), s: Uint8Array(32), v: number }
-
Convert to bigint
const rBigint = bytesToBigint(sig.r);
const sBigint = bytesToBigint(sig.s);
-
Recover yParity
// Try v=0 and v=1, see which recovers to correct address
let yParity = 0;
try {
const recovered = Secp256k1.recoverPublicKey({ r, s, v: 0 }, messageHash);
const recoveredAddress = addressFromPublicKey(recovered);
if (!equals(recoveredAddress, unsigned.address)) {
yParity = 1;
}
} catch {
yParity = 1;
}
-
Return signed authorization
return {
chainId: unsigned.chainId,
address: unsigned.address,
nonce: unsigned.nonce,
yParity,
r: rBigint,
s: sBigint
};
Signature Determinism
secp256k1 signing is deterministic (RFC 6979):
- Same private key + message always produces same signature
- Prevents nonce reuse attacks
- Signatures are reproducible
verify
Recover authority (signer) from authorization signature.
Process:
- Validate authorization structure
- Hash unsigned portion
- Recover public key from signature
- Derive address from public key
Namespace API
Factory API
Authorization.verify(auth: Authorization.Item): Address
Parameters:
auth: Signed authorization to verify
Returns: Recovered signer address (authority)Throws: ValidationError if validation fails or recovery failsExample:import { Authorization } from 'tevm';
const auth: Authorization.Item = {
chainId: 1n,
address: contractAddress,
nonce: 0n,
yParity: 0,
r: 0x123...n,
s: 0x456...n
};
try {
const authority = Authorization.verify(auth);
console.log(`Authorized by: ${authority}`);
// Verify it's the expected signer
if (authority === expectedEOA) {
console.log('Valid authorization from expected account');
}
} catch (e) {
console.error(`Verification failed: ${e}`);
}
Authorization.Verify({
keccak256,
rlpEncode,
recoverPublicKey,
addressFromPublicKey
})(auth: Authorization.Item): Address
Tree-shakeable factory with explicit crypto dependencies.Dependencies:
keccak256: (data: Uint8Array) => Uint8Array - Keccak256 hash function
rlpEncode: (data: Array<Uint8Array>) => Uint8Array - RLP encode function
recoverPublicKey: (signature, messageHash) => Uint8Array - secp256k1 recovery
addressFromPublicKey: (x: bigint, y: bigint) => Address - Address derivation
Example:import { Authorization } from 'tevm';
import { hash as keccak256 } from 'tevm/crypto/Keccak256';
import { encode as rlpEncode } from 'tevm/Rlp';
import { recoverPublicKey } from 'tevm/crypto/Secp256k1';
import { fromPublicKey as addressFromPublicKey } from 'tevm/Address';
const verify = Authorization.Verify({
keccak256,
rlpEncode,
recoverPublicKey,
addressFromPublicKey
});
const auth: Authorization.Item = {
chainId: 1n,
address: contractAddress,
nonce: 0n,
yParity: 0,
r: 0x123...n,
s: 0x456...n
};
try {
const authority = verify(auth);
console.log(`Authorized by: ${authority}`);
} catch (e) {
console.error(`Verification failed: ${e}`);
}
Bundle size: Only includes imported crypto dependencies.
Implementation Details
Verification Process:
-
Validate Structure
validate(auth); // Throws if invalid
-
Hash Unsigned Portion
const unsigned = {
chainId: auth.chainId,
address: auth.address,
nonce: auth.nonce
};
const messageHash = hash(unsigned);
-
Convert Signature to Bytes
const r = bigintToBytes(auth.r, 32);
const s = bigintToBytes(auth.s, 32);
-
Recover Public Key
const signature = { r, s, v: auth.yParity };
const publicKey = Secp256k1.recoverPublicKey(signature, messageHash);
// Returns 64-byte uncompressed public key (x || y)
-
Derive Address
const x = publicKey.slice(0, 32);
const y = publicKey.slice(32, 64);
return Address.fromPublicKey(x, y);
// keccak256(publicKey)[12:32]
ECDSA Recovery
Public key recovery uses ECDSA mathematics:
Given signature (r, s, v) and message hash h:
- Compute point R from r and v
- Compute s_inv = s^-1 mod n
- Recover public key: Q = s_inv * (h * G + r * R)
- Derive address from Q
The yParity (v) indicates which of two possible points to use.
Complete Signing Flow
Create, Sign, Verify
import { Authorization, Address } from 'tevm';
// 1. Create unsigned authorization
const unsigned: Authorization.Unsigned = {
chainId: 1n,
address: Address('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb2'),
nonce: 0n
};
// 2. Calculate hash (optional - done internally by sign)
const sigHash = Authorization.hash.call(unsigned);
console.log(`Signing hash: ${sigHash}`);
// 3. Sign with private key
const privateKey = Bytes32();
// ... your private key
const auth = Authorization.sign.call(unsigned, privateKey);
// 4. Validate signature
Authorization.validate.call(auth);
console.log('Signature valid');
// 5. Verify signer
const authority = Authorization.verify.call(auth);
console.log(`Signed by: ${authority}`);
// 6. Confirm it's your account
const myAddress = Address(privateKey);
console.log(`Match: ${Address.equals(authority, myAddress)}`);
Signature Security
Private Key Safety
Never expose private keys:
// Good: Keep private key secure
const privateKey = await getSecurePrivateKey();
const auth = Authorization.sign.call(unsigned, privateKey);
// Good: Clear after use
privateKey.fill(0);
// Bad: Hardcoded private key
const privateKey = new Uint8Array([1, 2, 3, ...]); // NEVER DO THIS
Nonce Management
Use correct nonce to prevent signature reuse:
import { Authorization } from 'tevm';
async function createAuthorization(
chainId: bigint,
delegateTo: Address,
privateKey: Uint8Array
): Promise<Authorization.Item> {
// Get current account nonce
const myAddress = Address(privateKey);
const nonce = await getAccountNonce(myAddress);
const unsigned = { chainId, address: delegateTo, nonce };
return Authorization.sign.call(unsigned, privateKey);
}
Chain ID Protection
Always use correct chain ID:
import { Authorization } from 'tevm';
const MAINNET = 1n;
const POLYGON = 137n;
// Good: Explicit chain ID
const unsigned = {
chainId: MAINNET,
address: contractAddress,
nonce: 0n
};
// Bad: Reusing authorization cross-chain
// Authorization signed for mainnet (1) won't work on polygon (137)
Signature Malleability
sign() automatically creates non-malleable signatures (s ≤ N/2):
import { Authorization } from 'tevm';
const auth = Authorization.sign.call(unsigned, privateKey);
// Signature is guaranteed non-malleable
console.log(auth.s <= Authorization.SECP256K1_HALF_N); // true
// Validation will pass
Authorization.validate.call(auth);
Advanced Patterns
Batch Signing
Sign multiple authorizations:
import { Authorization } from 'tevm';
function signAuthBatch(
unsignedList: Authorization.Unsigned[],
privateKey: Uint8Array
): Authorization.Item[] {
return unsignedList.map(unsigned =>
Authorization.sign.call(unsigned, privateKey)
);
}
const batch = signAuthBatch([
{ chainId: 1n, address: contract1, nonce: 0n },
{ chainId: 1n, address: contract2, nonce: 1n },
{ chainId: 1n, address: contract3, nonce: 2n }
], privateKey);
Verify Batch
Verify all signatures and collect authorities:
import { Authorization } from 'tevm';
function verifyAuthBatch(authList: Authorization.Item[]): {
authorities: Address[];
valid: Authorization.Item[];
invalid: Array<{ auth: Authorization.Item; error: string }>;
} {
const authorities: Address[] = [];
const valid: Authorization.Item[] = [];
const invalid: Array<{ auth: Authorization.Item; error: string }> = [];
for (const auth of authList) {
try {
const authority = Authorization.verify.call(auth);
authorities.push(authority);
valid.push(auth);
} catch (e) {
invalid.push({
auth,
error: e instanceof Error ? e.message : String(e)
});
}
}
return { authorities, valid, invalid };
}
const { authorities, valid, invalid } = verifyAuthBatch(authList);
console.log(`Valid: ${valid.length}, Invalid: ${invalid.length}`);
Pre-compute Hash
Pre-compute signing hash for UI display:
import { Authorization } from 'tevm';
async function requestSignature(
unsigned: Authorization.Unsigned
): Promise<Authorization.Item> {
// Pre-compute hash for user confirmation
const sigHash = Authorization.hash.call(unsigned);
// Show to user
console.log(`You are signing:`);
console.log(` Chain: ${unsigned.chainId}`);
console.log(` Delegate to: ${unsigned.address}`);
console.log(` Nonce: ${unsigned.nonce}`);
console.log(` Hash: ${sigHash}`);
// Get confirmation
const confirmed = await confirmWithUser();
if (!confirmed) {
throw new Error('User rejected signature');
}
// Sign
const privateKey = await getPrivateKey();
return Authorization.sign.call(unsigned, privateKey);
}
Verify Expected Signer
Verify signature is from expected account:
import { Authorization, Address } from 'tevm';
function verifyExpectedSigner(
auth: Authorization.Item,
expectedSigner: Address
): boolean {
try {
const authority = Authorization.verify.call(auth);
return Address.equals(authority, expectedSigner);
} catch {
return false;
}
}
const isValid = verifyExpectedSigner(auth, userEOA);
if (!isValid) {
throw new Error('Authorization not from expected signer');
}
Operation Costs
| Operation | Time | Notes |
|---|
hash | O(1) | RLP encode + keccak256 |
sign | O(1) | secp256k1 signing |
verify | O(1) | Public key recovery |
All operations are constant time with respect to input size.
Optimization Tips
- Cache hashes - Reuse hash if signing same unsigned multiple times
- Batch verification - Process multiple auths together
- Pre-validate - Call validate() before verify() to fail fast
- Parallel signing - Sign multiple auths in parallel (if private key allows)
Benchmarks
Typical performance (actual values depend on hardware):
hash: ~50,000 ops/sec
sign: ~10,000 ops/sec (includes ECDSA signing)
verify: ~8,000 ops/sec (includes public key recovery)
Testing
Test Signing & Verification
import { Authorization, Address } from 'tevm';
// Create test private key
const privateKey = Bytes32();
privateKey.fill(1);
const myAddress = Address(privateKey);
// Create unsigned
const unsigned: Authorization.Unsigned = {
chainId: 1n,
address: Address('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb2'),
nonce: 0n
};
// Sign
const auth = Authorization.sign.call(unsigned, privateKey);
// Verify signature is valid
Authorization.validate.call(auth);
// Verify recovers correct signer
const authority = Authorization.verify.call(auth);
expect(Address.equals(authority, myAddress)).toBe(true);
Test Hash Determinism
import { Authorization } from 'tevm';
const unsigned: Authorization.Unsigned = {
chainId: 1n,
address: contractAddress,
nonce: 0n
};
// Hash should be deterministic
const hash1 = Authorization.hash.call(unsigned);
const hash2 = Authorization.hash.call(unsigned);
expect(hash1).toEqual(hash2);
See Also