китаев.tech

Подпись сообщений — EIP-191

Как подписать текстовое сообщение через MetaMask и ethers.js v6, проверить подпись через EIP-191 и восстановить адрес подписанта.

Что такое подпись сообщения

Подпись сообщения доказывает, что пользователь контролирует приватный ключ адреса.

Это не транзакция. Сообщение не попадает в блокчейн, gas не тратится, State не меняется. Такой механизм используют для входа через кошелёк, подтверждения владения адресом и off-chain действий.


Подписываем сообщение и проверяем адрес

typescript
import { BrowserProvider, verifyMessage } from "ethers";
import type { Eip1193Provider } from "ethers";

declare global {
  interface Window {
    ethereum?: Eip1193Provider;
  }
}

const app = document.createElement("main");
app.innerHTML = `
  <button id="sign-message">Подписать сообщение</button>
  <pre id="signature-info">Сообщение не подписано</pre>
`;
document.body.appendChild(app);

const button = document.querySelector<HTMLButtonElement>("#sign-message");
const output = document.querySelector<HTMLPreElement>("#signature-info");

if (!button || !output) {
  throw new Error("Не удалось найти элементы интерфейса");
}

async function signEip191Message() {
  if (!window.ethereum) {
    output.textContent = "MetaMask не найден. Установите расширение браузера.";
    return;
  }

  const provider = new BrowserProvider(window.ethereum);

  await provider.send("eth_requestAccounts", []);

  const signer = await provider.getSigner();
  const address = await signer.getAddress();
  const message = `Вход на kitaev.tech
Адрес: ${address}
Nonce: 2026-06-20T14:00:00.000Z`;

  const signature = await signer.signMessage(message);
  const recoveredAddress = verifyMessage(message, signature);
  const isValid = recoveredAddress.toLowerCase() === address.toLowerCase();

  output.textContent = [
    `Адрес: ${address}`,
    `Сообщение:`,
    message,
    ``,
    `Подпись: ${signature}`,
    `Восстановленный адрес: ${recoveredAddress}`,
    `Подпись валидна: ${isValid}`,
  ].join("\n");
}

button.addEventListener("click", () => {
  signEip191Message().catch((error: unknown) => {
    const message = error instanceof Error ? error.message : "Неизвестная ошибка";
    output.textContent = `Ошибка подписи: ${message}`;
  });
});

MetaMask покажет пользователю текст сообщения и попросит подтвердить подпись. До подтверждения сайт не получает подпись.


Как это работает

provider.getSigner() — получает signer для аккаунта, выбранного в MetaMask.

signer.signMessage(message) — подписывает сообщение по EIP-191. ethers добавляет служебный префикс перед подписью.

verifyMessage(message, signature) — восстанавливает адрес, приватный ключ которого создал подпись.

recoveredAddress === address — проверяет, что подпись действительно принадлежит ожидаемому адресу.

Nonce — одноразовое значение. В реальном логине backend должен выдавать новый nonce для каждой попытки входа.


Что именно подписывает EIP-191

Кошелёк не подписывает строку напрямую. Для обычной personal message подписи к сообщению добавляется префикс:

text
\x19Ethereum Signed Message:
<длина сообщения>
<сообщение>

Этот префикс защищает пользователя от ситуации, где обычный текст можно выдать за транзакцию или структурированные данные другого формата.

Если строка выглядит как "0x1234", signMessage() подпишет её как шесть UTF-8 символов. Чтобы подписать байты 0x12 и 0x34, нужно передать Uint8Array.


Частые ошибки

Использовать подпись как транзакцию → EIP-191 подпись ничего не отправляет в сеть. Она только доказывает контроль адреса.

Подписывать один и тот же текст для логина → добавляйте nonce, домен и срок действия. Иначе подпись можно переиспользовать.

Проверять подпись без исходного сообщения → для verifyMessage() нужны и сообщение, и подпись. Если текст изменится хотя бы на один символ, адрес не восстановится корректно.


Что дальше

Материалы китаev.tech публикуются в образовательных целях и не являются инвестиционной рекомендацией. Примеры кода и описания протоколов — для обучения; использование в продакшне на ваш собственный риск. Дисклеймер и политика конфиденциальности.