Подпись сообщений и аутентификация пользователей

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

Кошельки Solana подписывают сообщения двумя способами:

  • Офчейн-сообщения (solana:signOffchainMessage) оборачивают ваш текст в стандартный формат, определённый в спецификации офчейн-подписи сообщений (OCMS). Этот формат невозможно спутать с транзакцией, в нём указаны аккаунты, которые должны его подписать, и именно этот формат подписывают аппаратные кошельки.
  • Необработанные сообщения (solana:signMessage) подписывают ровно те байты, которые передаёт ваше приложение, без префикса и без объявленных подписантов. Их поддерживает большинство программных кошельков, но ничто в этих байтах не указывает, что это сообщение, а не транзакция.

Используйте офчейн-сообщения там, где кошелёк их поддерживает, а там, где нет, — переходите на необработанные сообщения. В остальной части руководства в качестве примера разобран вход в систему, но те же шаги подписи и проверки применимы к любому сообщению.

Выбор формата сообщения

Офчейн-сообщениеНеобработанное сообщениеТранзакция
Функция кошелькаsignOffchainMessagesignMessagesignTransaction
Поддержка аппаратных кошельковЗависит от устройстваНетДа
Читаемо для пользователяДаТолько в программных кошелькахЗависит от устройства
Объявляет обязательных подписантовДаНетДа
RPC-вызовы и комиссииНетНетBlockhash, отправка, комиссия, подтверждение

Почему не транзакция?

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

  • Сетевые комиссии. За транзакцию взимается комиссия, даже если она ничего не делает.
  • Множество RPC-вызовов. Ваше приложение получает blockhash, симулирует и отправляет транзакцию, а затем опрашивает сеть в ожидании подтверждения.
  • Дополнительная задержка. Каждый такой вызов — это круговой обмен данными до того, как пользователь войдёт в систему.
  • Ничего читаемого. Пользователь подтверждает байты транзакции, а не утверждение о том, с чем он соглашается.
  • Поведенческий риск. Если просить пользователей подписывать транзакцию только ради входа, их проще обманом заставить подписать вредоносную.

Офчейн-сообщение обходит всё это. Единственный круговой обмен данными — с вашим собственным сервером.

A sign-in transaction fetches a blockhash, signs, simulates, sends, and polls for confirmation before the server can verify it. An off-chain message goes from a server challenge to a wallet signature to a server-side check, with no RPC calls and no fee.

Что подписывает кошелёк

Офчейн-сообщение начинается с фиксированного домена подписи \xffsolana offchain. Ни одна допустимая транзакция не начинается с этих байтов, поэтому подписанное сообщение невозможно использовать как транзакцию. Затем в версии 1 формата перечисляются обязательные подписанты, а само сообщение передаётся как текст в UTF-8 без ограничения длины.

Кошелёк формирует эти байты, подписывает их и возвращает вместе с подписью. Полный формат, включая устаревшую версию 0, описан в спецификации.

A version 1 off-chain message is a 16-byte signing domain, a version byte, a signer count, each signer's 32-byte public key in sorted order, and the UTF-8 content. The envelope prefixes it with a signature count and one 64-byte signature per signer.

Выбор способа входа

Не все кошельки уже поддерживают офчейн-сообщения. Прежде чем запрашивать подпись, проверьте возможности подключённого аккаунта и переходите на запасной вариант, если нужной возможности нет:

  1. Если аккаунт поддерживает solana:signOffchainMessage с версией сообщения 1, подпишите офчейн-сообщение.
  2. В противном случае подпишите тот же запрос через solana:signMessage. Его поддерживает большинство программных кошельков.
  3. Для пользователей аппаратных кошельков без поддержки сообщений предусмотрите явное согласие, например переключатель «Я использую аппаратный кошелёк», при котором вместо этого подписывается транзакция для входа. Не направляйте их по этому пути по умолчанию.
When a wallet connects, check whether its account supports solana:signOffchainMessage version 1. If it does, sign an off-chain message. If not, sign a raw message with solana:signMessage, and let hardware wallet users opt into signing a sign-in transaction.

Подпись сообщения в браузере

Установите пакеты Wallet Standard вместе с Kit:

Terminal
$
npm install @solana/kit @solana/wallet-standard-features @wallet-standard/ui @wallet-standard/ui-features @wallet-standard/ui-registry

Эти хелперы принимают UiWalletAccount, например account, который возвращает useConnectedWallet в руководстве по React:

offchain-message.ts
import {
SolanaSignOffchainMessage,
type SolanaSignOffchainMessageFeature
} from "@solana/wallet-standard-features";
import type { UiWalletAccount } from "@wallet-standard/ui";
import { getWalletAccountFeature } from "@wallet-standard/ui-features";
import { getWalletAccountForUiWalletAccount } from "@wallet-standard/ui-registry";
type OffchainMessageFeature =
SolanaSignOffchainMessageFeature[typeof SolanaSignOffchainMessage];
export function supportsOffchainMessages(account: UiWalletAccount) {
if (!account.features.includes(SolanaSignOffchainMessage)) return false;
const feature = getWalletAccountFeature(
account,
SolanaSignOffchainMessage
) as OffchainMessageFeature;
return feature.supportedMessageVersions.includes(1);
}
export async function signOffchainMessage(
account: UiWalletAccount,
message: string
) {
const feature = getWalletAccountFeature(
account,
SolanaSignOffchainMessage
) as OffchainMessageFeature;
const [output] = await feature.signOffchainMessage({
messageVersion: 1,
account: getWalletAccountForUiWalletAccount(account),
message,
requiredSigners: [account.publicKey]
});
return output;
}

signOffchainMessage возвращает signedOffchainMessage — полные байты, которые подписал кошелёк, — и signature. Закодируйте оба значения в Base64 и отправьте на сервер вместе с адресом кошелька.

Составьте запрос так, чтобы он хорошо читался на небольшом экране. Укажите название приложения и домен, объясните, что означает подпись, и добавьте одноразовый nonce и время выдачи с вашего сервера:

swap.app wants you to sign in with your Solana account.
Signing in costs nothing and does not submit a transaction.
Nonce: 8f3a1c9e
Issued at: 2026-09-29T17:04:00Z

Проверка подписи на сервере

Проверка подписи выполняется локально и не требует RPC-соединения. Декодируйте байты, подписанные кошельком, убедитесь, что они совпадают с выданным вами запросом, а затем проверьте подпись:

verify-sign-in.ts
import {
address,
assertOffchainMessageV1Equal,
getBase64Encoder,
getOffchainMessageDecoder,
verifyOffchainMessageEnvelope,
type OffchainMessageBytes,
type SignatureBytes
} from "@solana/kit";
export async function verifySignIn(input: {
walletAddress: string;
expectedMessage: string;
signedOffchainMessage: string;
signature: string;
}) {
const signer = address(input.walletAddress);
const base64 = getBase64Encoder();
const content = base64.encode(
input.signedOffchainMessage
) as OffchainMessageBytes;
const signature = base64.encode(input.signature) as SignatureBytes;
const received = getOffchainMessageDecoder().decode(content);
if (received.version !== 1) {
throw new Error(`Expected a v1 message, got v${received.version}`);
}
assertOffchainMessageV1Equal(received, {
version: 1,
requiredSignatories: [{ address: signer }],
content: input.expectedMessage
});
await verifyOffchainMessageEnvelope({
content,
signatures: { [signer]: signature }
});
}

assertOffchainMessageV1Equal выбрасывает ошибку, если содержимое или подписанты отличаются от ожидаемых. verifyOffchainMessageEnvelope выбрасывает ошибку, если подпись отсутствует или недействительна. Находите expectedMessage по nonce на своём сервере, а не доверяйте копии, которую присылает клиент. Погасите nonce сразу после успешной проверки, чтобы одну и ту же подпись нельзя было использовать для входа дважды.

Проверка необработанного сообщения

Подпись signMessage покрывает байты запроса напрямую, поэтому клиенту достаточно отправить подпись и адрес кошелька. Проверьте её по выданному вам запросу:

verify-raw-sign-in.ts
import {
address,
getBase64Encoder,
getPublicKeyFromAddress,
getUtf8Encoder,
signatureBytes,
verifySignature
} from "@solana/kit";
export async function verifyRawSignIn(input: {
walletAddress: string;
expectedMessage: string;
signature: string;
}) {
const publicKey = await getPublicKeyFromAddress(address(input.walletAddress));
const signature = signatureBytes(getBase64Encoder().encode(input.signature));
const message = getUtf8Encoder().encode(input.expectedMessage);
if (!(await verifySignature(publicKey, signature, message))) {
throw new Error("Invalid signature");
}
}

Действуют те же правила для nonce: находите expectedMessage по nonce и гасите его после успешной проверки.

@solana/kit реэкспортирует эти хелперы из @solana/offchain-messages. Используйте отдельный пакет, если ваш сервер не зависит от Kit.

Sign In With Solana

Sign In With Solana (SIWS) стандартизирует сам запрос. Ваше приложение передаёт структурированные поля, такие как domain, statement, nonce и issuedAt, в функцию кошелька solana:signIn. Кошелёк форматирует их в стандартное сообщение, при необходимости подключается и подписывает его за один шаг.

Версия 1.1.0 функции solana:signIn добавляет опцию useOffchainMessage. Если передать useOffchainMessage: { messageVersion: 1 }, кошелёк оборачивает текст SIWS в офчейн-сообщение версии 1, где единственным обязательным подписантом является подписывающий аккаунт. Аппаратные кошельки могут отобразить и подписать такое сообщение, а в результате будет указано signedMessageFormat: { kind: "offchainMessage", messageVersion: 1 }. Кошельки, которые заявляют версию 1.0.0, эту опцию не знают, поэтому сначала проверьте версию функции, а если она ниже — не передавайте опцию.

solana:signIn — это функция кошелька, а не аккаунта, поэтому эти хелперы принимают UiWallet:

sign-in.ts
import {
SolanaSignIn,
type SolanaSignInFeature,
type SolanaSignInInput
} from "@solana/wallet-standard-features";
import type { UiWallet } from "@wallet-standard/ui";
import { getWalletFeature } from "@wallet-standard/ui-features";
type SignInFeature = SolanaSignInFeature[typeof SolanaSignIn];
export function supportsOffchainSignIn(wallet: UiWallet) {
if (!wallet.features.includes(SolanaSignIn)) return false;
const feature = getWalletFeature(wallet, SolanaSignIn) as SignInFeature;
return feature.version === "1.1.0";
}
export async function signIn(wallet: UiWallet, input: SolanaSignInInput) {
const feature = getWalletFeature(wallet, SolanaSignIn) as SignInFeature;
const [output] = await feature.signIn({
...input,
...(supportsOffchainSignIn(wallet) && {
useOffchainMessage: { messageVersion: 1 }
})
});
return output;
}

Сформируйте входные данные на сервере с одноразовым nonce и временем issuedAt и сохраните копию, привязанную к nonce. Погасите nonce после успешной проверки входа.

Без Wallet Adapter verifySignIn из @solana/wallet-standard-util проверяет только обычный вывод SIWS. Если signedMessageFormat указывает на офчейн-сообщение, signedMessage — это офчейн-сообщение, содержимое которого является текстом SIWS. Восстановите ожидаемый текст по сохранённым входным данным и адресу подписанта с помощью createSignInMessageText из @solana/wallet-standard-util, а затем проверьте его как любое другое офчейн-сообщение, как описано в разделе Проверка подписи на сервере. Этот процесс в Kit описан в README пакета @solana/offchain-messages.

Для кошельков

Кошельки добавляют поддержку, реализуя функцию Wallet Standard solana:signOffchainMessage из @solana/wallet-standard-features:

  • Укажите version: "1.0.0" и перечислите версии сообщений, которые вы можете подписывать, в supportedMessageVersions. Сейчас это [1].
  • Сформируйте преамбулу версии 1 из requiredSigners, отсортировав список и проверив его на дубликаты. Отклоняйте запрос, если аккаунт не входит в число обязательных подписантов.
  • Показывайте пользователю текст сообщения полностью перед подписанием, в том числе на аппаратных устройствах.
  • Верните точные байты, которые вы подписали, в поле signedOffchainMessage вместе с подписью Ed25519 signature.
  • Чтобы поддерживать off-chain Sign In With Solana, указывайте version: "1.1.0" для solana:signIn. Если во входных данных есть useOffchainMessage, подпишите текст SIWS как off-chain сообщение версии 1, верните его байты в signedMessage и задайте signedMessageFormat.
  • Продолжайте поддерживать solana:signMessage. Он не изменился, и приложения по-прежнему используют его как резервный вариант.

Is this page helpful?