Ký một thông điệp chứng minh người dùng kiểm soát một ví, mà không cần giao dịch, không mất phí và không phát sinh lệnh gọi mạng. Các ứng dụng dùng thông điệp đã ký để đăng nhập người dùng, ghi nhận việc người dùng chấp nhận điều khoản, liên kết ví với tài khoản hiện có, hoặc ủy quyền một hành động diễn ra off-chain, chẳng hạn như đặt lệnh.
Ví Solana ký thông điệp theo hai cách:
- Thông điệp off-chain (
solana:signOffchainMessage) đóng gói văn bản của bạn theo một định dạng chuẩn, được định nghĩa bởi đặc tả ký thông điệp off-chain (OCMS). Định dạng này không bao giờ bị nhầm với giao dịch, nêu rõ các tài khoản phải ký, và là định dạng mà ví phần cứng ký. - Thông điệp thô (
solana:signMessage) ký chính xác các byte mà ứng dụng của bạn truyền vào, không có tiền tố và không khai báo người ký. Hầu hết ví phần mềm đều hỗ trợ, nhưng không có gì trong các byte đánh dấu đó là thông điệp thay vì giao dịch.
Hãy dùng thông điệp off-chain ở nơi ví hỗ trợ, và quay về thông điệp thô ở nơi ví không hỗ trợ. Phần còn lại của hướng dẫn này lấy đăng nhập làm ví dụ, nhưng các bước ký và xác minh tương tự áp dụng cho mọi thông điệp.
Chọn định dạng thông điệp
| Thông điệp off-chain | Thông điệp thô | Giao dịch | |
|---|---|---|---|
| Tính năng của ví | signOffchainMessage | signMessage | signTransaction |
| Hỗ trợ ví phần cứng | Tùy thiết bị | Không | Có |
| Người dùng đọc được | Có | Chỉ ví phần mềm | Tùy thiết bị |
| Khai báo người ký bắt buộc | Có | Không | Có |
| Lệnh gọi RPC và phí | Không có | Không có | Blockhash, gửi, phí, xác nhận |
Tại sao không dùng giao dịch?
Trước đây, ví phần cứng không có cách nào để ký một thông điệp có thể đọc được. Nhiều ứng dụng đành yêu cầu những người dùng này ký một giao dịch không làm gì (no-op) thay thế. Cách này dùng được, nhưng làm tăng độ trễ, chi phí và rủi ro:
- Phí mạng. Giao dịch phải trả phí, ngay cả khi không làm gì.
- Nhiều lệnh gọi RPC. Ứng dụng của bạn lấy blockhash, mô phỏng, gửi, rồi thăm dò để chờ xác nhận.
- Độ trễ tăng thêm. Mỗi lệnh gọi đó là một vòng đi-về trước khi người dùng được đăng nhập.
- Không đọc được. Người dùng phê duyệt các byte giao dịch, chứ không phải một tuyên bố về nội dung họ đồng ý.
- Rủi ro hành vi. Yêu cầu người dùng ký giao dịch chỉ để đăng nhập khiến họ dễ bị lừa ký một giao dịch có hại hơn.
Thông điệp off-chain bỏ qua tất cả những điều đó. Vòng đi-về duy nhất là tới máy chủ của chính bạn.
Ví ký những gì
Thông điệp off-chain bắt đầu bằng một miền ký (signing domain) cố định, \xffsolana offchain. Không giao dịch hợp lệ nào bắt đầu bằng các byte đó, nên thông điệp đã ký không bao giờ có thể bị dùng như một giao dịch. Phiên bản 1 của định dạng sau đó liệt kê các người ký bắt buộc và mang thông điệp dưới dạng văn bản UTF-8, không giới hạn độ dài.
Ví tạo các byte này, ký chúng, và trả về cùng với chữ ký. Đặc tả bao quát toàn bộ định dạng, bao gồm phiên bản 0 cũ.
Chọn phương thức đăng nhập
Chưa phải ví nào cũng hỗ trợ thông điệp off-chain. Hãy kiểm tra các tính năng của tài khoản đã kết nối trước khi yêu cầu chữ ký, và dùng phương án dự phòng khi thiếu tính năng:
- Nếu tài khoản hỗ trợ
solana:signOffchainMessagevới phiên bản thông điệp 1, hãy ký một thông điệp off-chain. - Nếu không, hãy ký cùng thử thách (challenge) đó bằng
solana:signMessage. Hầu hết ví phần mềm đều hỗ trợ. - Hãy cho người dùng ví phần cứng không hỗ trợ thông điệp một lựa chọn tham gia rõ ràng, chẳng hạn như nút bật "Tôi đang dùng ví phần cứng", để thay vào đó ký một giao dịch đăng nhập. Đừng mặc định đưa họ vào hướng này.
Ký thông điệp trong trình duyệt
Cài đặt các gói Wallet Standard cùng với Kit:
$npm install @solana/kit @solana/wallet-standard-features @wallet-standard/ui @wallet-standard/ui-features @wallet-standard/ui-registry
Các hàm trợ giúp này nhận một UiWalletAccount, chẳng hạn như account do
useConnectedWallet trả về trong hướng dẫn React:
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 trả về signedOffchainMessage, toàn bộ các byte mà ví đã ký, và signature. Hãy mã hóa cả hai bằng Base64 và gửi chúng đến máy chủ cùng với địa chỉ ví.
Hãy viết thử thách sao cho dễ đọc trên màn hình nhỏ. Nêu tên ứng dụng và tên miền của bạn, cho biết việc ký làm gì, và đưa vào một nonce dùng một lần cùng thời điểm phát hành từ máy chủ của bạn:
swap.app wants you to sign in with your Solana account.Signing in costs nothing and does not submit a transaction.Nonce: 8f3a1c9eIssued at: 2026-09-29T17:04:00Z
Xác minh chữ ký trên máy chủ của bạn
Việc xác minh là kiểm tra chữ ký cục bộ và không cần kết nối RPC. Giải mã các byte mà ví đã ký, xác nhận chúng khớp với thử thách bạn đã phát hành, rồi kiểm tra chữ ký:
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 sẽ ném lỗi nếu nội dung hoặc người ký khác với những gì bạn mong đợi. verifyOffchainMessageEnvelope ném lỗi nếu chữ ký bị thiếu hoặc không hợp lệ. Hãy tra cứu expectedMessage theo nonce trên máy chủ của bạn thay vì tin vào bản sao mà client gửi lại. Hãy tiêu thụ nonce ngay khi xác minh thành công, để cùng một chữ ký không thể đăng nhập hai lần.
Xác minh thông điệp thô
Chữ ký signMessage bao phủ trực tiếp các byte của thử thách, nên client chỉ cần gửi chữ ký và địa chỉ ví. Hãy kiểm tra nó với thử thách bạn đã phát hành:
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");}}
Áp dụng cùng quy tắc nonce: tra cứu expectedMessage theo nonce và tiêu thụ nó ngay khi xác minh thành công.
@solana/kit xuất lại các hàm trợ giúp này từ @solana/offchain-messages. Hãy dùng gói độc lập nếu máy chủ của bạn không phụ thuộc vào Kit.
Sign In With Solana
Sign In With Solana (SIWS) chuẩn hóa chính thử thách. Ứng dụng của bạn truyền các trường có cấu trúc, như domain, statement, nonce và issuedAt, đến tính năng solana:signIn của ví. Ví định dạng chúng thành một thông điệp chuẩn, kết nối nếu cần, và ký trong một bước.
Phiên bản 1.1.0 của solana:signIn bổ sung tùy chọn useOffchainMessage. Khi bạn truyền useOffchainMessage: { messageVersion: 1 }, ví sẽ đóng gói văn bản SIWS trong một thông điệp off-chain phiên bản 1 với tài khoản ký là người ký bắt buộc duy nhất. Ví phần cứng có thể hiển thị và ký nó, và đầu ra báo cáo signedMessageFormat: { kind: "offchainMessage", messageVersion: 1 }. Các ví công bố phiên bản 1.0.0 không biết tùy chọn này, vì vậy hãy kiểm tra phiên bản tính năng trước và bỏ qua tùy chọn nếu không phải phiên bản 1.1.0.
solana:signIn là tính năng của ví, không phải tính năng của tài khoản, nên các
hàm trợ giúp này nhận một UiWallet:
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;}
Tạo đầu vào trên máy chủ của bạn, với nonce dùng một lần và thời điểm issuedAt, và giữ một bản sao được đánh khóa theo nonce. Hãy tiêu thụ nonce ngay khi đăng nhập được xác minh.
Nếu không dùng Wallet Adapter, verifySignIn từ @solana/wallet-standard-util chỉ kiểm tra đầu ra SIWS thuần. Khi signedMessageFormat báo là thông điệp off-chain, signedMessage là một thông điệp off-chain có nội dung là văn bản SIWS. Hãy dựng lại văn bản mong đợi từ đầu vào đã lưu và địa chỉ ký bằng createSignInMessageText từ @solana/wallet-standard-util, rồi xác minh như mọi thông điệp off-chain khác, như trong Xác minh chữ ký trên máy chủ của bạn. README của @solana/offchain-messages trình bày quy trình này trong Kit.
Dành cho ví
Ví bổ sung hỗ trợ bằng cách triển khai tính năng Wallet Standard solana:signOffchainMessage từ @solana/wallet-standard-features:
- Báo cáo
version: "1.0.0"và liệt kê các phiên bản thông điệp mà bạn có thể ký trongsupportedMessageVersions. Hiện nay, đó là[1]. - Tạo phần mở đầu (preamble) phiên bản 1 từ
requiredSigners, đồng thời sắp xếp và kiểm tra các mục trùng lặp. Từ chối yêu cầu nếu tài khoản không nằm trong danh sách người ký bắt buộc. - Hiển thị đầy đủ nội dung thông điệp cho người dùng trước khi ký, kể cả trên các thiết bị phần cứng.
- Trả về chính xác các byte bạn đã ký dưới dạng
signedOffchainMessage, cùng vớisignatureEd25519. - Để hỗ trợ Sign In With Solana ngoài chuỗi (off-chain), hãy báo cáo
version: "1.1.0"chosolana:signIn. Khi đầu vào cóuseOffchainMessage, hãy ký văn bản SIWS dưới dạng thông điệp off-chain phiên bản 1, trả về các byte của nó dưới dạngsignedMessage, và đặtsignedMessageFormat. - Tiếp tục hỗ trợ
solana:signMessage. Phương thức này không thay đổi và các ứng dụng vẫn dùng nó làm giải pháp dự phòng.
Is this page helpful?