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:
- EOA —
ecrecover(hash, sig) === address. Pure crypto, no RPC round-trip. - Contract —
eth_call address.isValidSignature(hash, sig)and check the first 4 bytes againstMAGIC_VALUE(0x1626ba7e). A revert is treated asfalse, 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.
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.