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

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

Кошельки 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.
  • Чтобы поддерживать офчейн-версию Sign In With Solana, укажите version: "1.1.0" для solana:signIn. Если во входных данных есть useOffchainMessage, подпишите текст SIWS как офчейн-сообщение версии 1, верните его байты в поле signedMessage и задайте signedMessageFormat.
  • Продолжайте поддерживать solana:signMessage. Он не изменился, и приложения по-прежнему используют его как запасной вариант.

Is this page helpful?