Guides/EVM

Verify substrate keys

Check ed25519/sr25519 signatures in contracts, and associate an EVM key with a hotkey.

View as Markdown

Ethereum tooling can only verify secp256k1 signatures (ecrecover), but Bittensor identity lives in substrate keys — sr25519 and ed25519. Two precompiles bridge the gap, letting a contract check "does the holder of this hotkey/coldkey approve this?" without any substrate extrinsic:

PrecompileAddressChecks
sr25519-verify0x…0403sr25519 — what btcli wallets use by default.
ed25519-verify0x…0402ed25519 — keys created with that crypto type.

Both have one function with the same shape:

function verify(bytes32 message, bytes32 publicKey, bytes32 r, bytes32 s)
    external pure returns (bool);

The message is exactly 32 bytes — you sign a digest of your payload, not the payload itself. The 64-byte substrate signature splits into r (first 32 bytes) and s (last 32).

Round trip from the terminal

Sign a digest with an SDK keypair, verify it through the precompile — no gas, no contract:

import hashlib
from bittensor.keyfiles import Keypair

keypair = Keypair.create_from_mnemonic("your twelve words …")  # sr25519 by default
message = hashlib.sha256(b"claim:0x1074Ad…").digest()          # any 32-byte digest
signature = keypair.sign(message)

print("message  ", "0x" + message.hex())
print("pubkey   ", keypair.ss58_address)
print("r        ", "0x" + signature[:32].hex())
print("s        ", "0x" + signature[32:].hex())
btcli evm call sr25519-verify verify 0xMESSAGE… 5Fpubkey… 0xR… 0xS…
# → true

(btcli evm call converts the ss58 address to the bytes32 public key; flip any byte of the signature and the call returns false.)

In a contract: gate an action on hotkey approval

The canonical use: an EVM airdrop or registry where claiming for a hotkey requires that hotkey's signature over the claimer's EVM address — so nobody can claim on someone else's identity:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface ISr25519Verify {
    function verify(bytes32 message, bytes32 publicKey, bytes32 r, bytes32 s)
        external pure returns (bool);
}

contract HotkeyClaim {
    ISr25519Verify constant VERIFY =
        ISr25519Verify(0x0000000000000000000000000000000000000403);

    mapping(bytes32 => address) public claimedBy;

    function claim(bytes32 hotkey, bytes32 r, bytes32 s) external {
        require(claimedBy[hotkey] == address(0), "already claimed");
        // the hotkey must have signed sha256("claim:" ++ caller address)
        bytes32 message = sha256(abi.encodePacked("claim:", msg.sender));
        require(VERIFY.verify(message, hotkey, r, s), "bad signature");
        claimedBy[hotkey] = msg.sender;
    }
}

The off-chain side produces r/s with the Python snippet above (signing sha256(b"claim:" + bytes.fromhex(evm_address[2:]))). Use ed25519-verify at 0x…0402 for ed25519 keys — same interface.

On-chain association: hotkey ↔ EVM key

Signature checks prove ownership per call. For a standing link the chain stores, associate the EVM key with a hotkey on a subnet — this is what makes uid-lookup resolve, and some subnets require it:

btcli evm associate --netuid 1 -w my_wallet -H my_hotkey
btcli query associated-evm-key --netuid 1 --hotkey 5F… --json

Under the hood this is the reverse direction of proof: the EVM key signs (EIP-191) over the hotkey's public key and a recent block number, and the hotkey submits associate-evm-key carrying that signature. In Python:

import bittensor as sub
from bittensor.evm import association_proof
from bittensor.evm.keys import unlock_evm_key

wallet = sub.Wallet(name="my_wallet", hotkey="my_hotkey")
account = unlock_evm_key("default", "my_wallet")

async with sub.Client("finney") as client:
    signature, block = association_proof(
        account, wallet.hotkey.ss58_address, await client.block()
    )
    result = await client.execute(
        sub.AssociateEvmKey(
            netuid=1, evm_key=account.address,
            block_number=block, signature=signature,
        ),
        wallet,  # associate_evm_key is signed by the hotkey
    )

There is also address-mapping (0x…080c), the chain's own h160 → mirror conversion as a precompile — useful when a contract needs the ss58 mirror of an address it only knows as h160:

btcli evm call address-mapping addressMapping 0x1074Ad…   # → bytes32 mirror pubkey

See also