Ethernauta

EIP-1271 — verify a signature

verify_message_deployed decides whether a signature is valid for a given address that already lives on-chain — an EOA or an already-deployed contract account. Two paths, tried in order:

  1. EOAecrecover(hash, sig) === address. Pure crypto, no RPC round-trip.
  2. Contracteth_call address.isValidSignature(hash, sig) and check the first 4 bytes against MAGIC_VALUE (0x1626ba7e). A revert is treated as false, not thrown.

This demo signs an arbitrary message with personal_sign and verifies it through the EOA path. Tampering with one byte flips the result.

MessageVerify me with EIP-1271
ChainSepolia

Sign in via Connect wallet at the top right to run this demo.

View on GitHub

The code

import "./demo.css"
import { eip155_11155111 } from "@ethernauta/chain/eip155-11155111"
import {
  AddressSchema,
  type Bytes,
  BytesSchema,
} from "@ethernauta/core"
import { verify_message_deployed } from "@ethernauta/crypto"
import { personal_sign } from "@ethernauta/eip/191"
import { useProvider } from "@ethernauta/react"
import {
  create_reader,
  encode_chain_id,
  http,
} from "@ethernauta/transport"
import { useState } from "react"
import { parse } from "valibot"
import { Button } from "../../components/button"
import { SignInHint } from "../../components/sign-in-hint"
import { use_session } from "../../lib/auth/use-session"
import { PROVIDER_STORE_KEY } from "../../lib/provider-store"

const SEPOLIA_CHAIN_ID = encode_chain_id({
  namespace: "eip155",
  reference: eip155_11155111.chainId,
})

const CHAINS = [
  {
    chainId: SEPOLIA_CHAIN_ID,
    transports: [
      http("https://ethereum-sepolia-rpc.publicnode.com"),
    ],
  },
]
const reader = create_reader(CHAINS)

const MESSAGE = "Verify me with EIP-1271"

export function Verify1271Demo() {
  const session = use_session()
  const owner = session
    ? parse(AddressSchema, session.address)
    : null
  const provider = useProvider({ key: PROVIDER_STORE_KEY })
  const [signature, set_signature] = useState<Bytes | null>(
    null,
  )
  const [valid, set_valid] = useState<boolean | null>(null)
  const [busy, set_busy] = useState(false)
  const [error, set_error] = useState<string | null>(null)

  async function sign_and_verify() {
    if (!owner || !provider) return
    set_busy(true)
    set_error(null)
    set_valid(null)
    set_signature(null)
    try {
      const raw_sig = await personal_sign([MESSAGE, owner])(
        provider.signer({ chain_id: SEPOLIA_CHAIN_ID }),
      )
      const sig = parse(BytesSchema, raw_sig)
      set_signature(sig)
      const ok = await verify_message_deployed({
        address: owner,
        message: MESSAGE,
        signature: sig,
      })(reader({ chain_id: SEPOLIA_CHAIN_ID }))
      set_valid(ok)
    } catch (e) {
      set_error(e instanceof Error ? e.message : String(e))
    } finally {
      set_busy(false)
    }
  }

  async function tamper_and_reverify() {
    if (!owner || !signature) return
    set_busy(true)
    set_error(null)
    try {
      const flipped = flip_last_byte(signature)
      const ok = await verify_message_deployed({
        address: owner,
        message: MESSAGE,
        signature: flipped,
      })(reader({ chain_id: SEPOLIA_CHAIN_ID }))
      set_valid(ok)
      set_signature(flipped)
    } catch (e) {
      set_error(e instanceof Error ? e.message : String(e))
    } finally {
      set_busy(false)
    }
  }

  return (
    <div className="verify-1271-root">
      <Row label="Message" value={MESSAGE} />
      <Row label="Chain" value="Sepolia" />
      {owner && <Row label="Owner" value={owner} mono />}
      {signature && (
        <Row label="Signature" value={signature} mono />
      )}
      {valid !== null && (
        <Row
          label="verify_message_deployed"
          value={valid ? "true ✓" : "false ✗"}
        />
      )}
      {error && (
        <p className="verify-1271-error">{error}</p>
      )}
      <div className="verify-1271-actions">
        {!owner && <SignInHint />}
        {owner && (
          <Button onClick={sign_and_verify} disabled={busy}>
            {busy ? "…" : "Sign + verify"}
          </Button>
        )}
        {signature && (
          <Button
            onClick={tamper_and_reverify}
            disabled={busy}
          >
            Tamper one byte
          </Button>
        )}
      </div>
    </div>
  )
}

function flip_last_byte(hex: Bytes): Bytes {
  const body = hex.slice(2)
  const last = body.slice(-2)
  const byte = (Number.parseInt(last, 16) ^ 0x01)
    .toString(16)
    .padStart(2, "0")
  return parse(BytesSchema, `0x${body.slice(0, -2)}${byte}`)
}

function Row({
  label,
  value,
  mono,
}: {
  label: string
  value: string
  mono?: boolean
}) {
  return (
    <div className="verify-1271-row">
      <span className="verify-1271-row-label">{label}</span>
      <span
        className={
          mono
            ? "verify-1271-row-value is-mono"
            : "verify-1271-row-value"
        }
      >
        {value}
      </span>
    </div>
  )
}

Why both paths matter

ERC-4337, ERC-5792, and EIP-7702 all assume smart accounts. Their signatures cannot be verified by anything downstream without an isValidSignature shim. EIP-1271 is the contract that closes that loop: dapps verify once, indifferent to whether the signer is an EOA or a smart account.