Ký một tin nhắn chứng minh rằng 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 có lệnh gọi mạng. Các ứng dụng dùng tin nhắn đã ký để đăng nhập cho người dùng, ghi nhận rằng 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ý tin nhắn theo hai cách:
- Tin nhắn off-chain (
solana:signOffchainMessage) bao bọc văn bản của bạn trong một định dạng chuẩn, được xác định bởi đặc tả ký tin nhắn off-chain (OCMS). Định dạng này không bao giờ bị nhầm lẫn với giao dịch, nêu rõ các tài khoản phải ký, và là định dạng mà ví cứng ký. - Tin nhắn 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 các ví phần mềm đều hỗ trợ loại này, nhưng không có gì trong các byte cho biết đó là tin nhắn chứ không phải giao dịch.
Hãy dùng tin nhắn off-chain khi ví hỗ trợ, và dự phòng bằng tin nhắn thô khi ví không hỗ trợ. Phần còn lại của hướng dẫn này dùng đăng nhập làm ví dụ minh họa, nhưng các bước ký và xác minh tương tự áp dụng cho mọi tin nhắn.
Chọn định dạng tin nhắn
| Tin nhắn off-chain | Tin nhắn thô | Giao dịch | |
|---|---|---|---|
| Tính năng của ví | signOffchainMessage | signMessage | signTransaction |
| Hỗ trợ ví 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í cứng không có cách nào để ký một tin nhắn đọc được. Nhiều ứng dụng đã chuyển sang yêu cầu những người dùng này ký một giao dịch no-op thay thế. Cách đó vẫn hoạt động, nhưng nó làm tăng độ trễ, chi phí và rủi ro:
- Phí mạng. Một giao dịch phải trả phí, ngay cả khi nó không làm gì cả.
- Nhiều lệnh gọi RPC. Ứng dụng của bạn lấy blockhash, mô phỏng, gửi, và sau đó thăm dò để chờ xác nhận.
- Độ trễ tăng thêm. Mỗi lệnh gọi đó là một lượt đi về trước khi người dùng được đăng nhập.
- Không có nội dung đọc được. Người dùng phê duyệt các byte của giao dịch, chứ không phải một tuyên bố về những gì họ đang đồ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.
Tin nhắn off-chain loại bỏ tất cả những điều đó. Lượt đi về duy nhất là tới máy chủ của chính bạn.
Ví ký những gì
Tin nhắn off-chain bắt đầu bằng một miền ký cố định, \xffsolana offchain.
Không giao dịch hợp lệ nào bắt đầu bằng các byte đó, nên một tin nhắn đã ký không bao giờ có thể
được dùng như 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 tin nhắn 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 cả phiên bản 0 cũ.
Chọn phương thức đăng nhập
Không phải ví nào cũng đã hỗ trợ tin nhắn off-chain. Hãy kiểm tra các tính năng của tài khoản đang kết nối trước khi yêu cầu chữ ký, và dự phòng khi tính năng bị thiếu:
- Nếu tài khoản hỗ trợ
solana:signOffchainMessagevới phiên bản tin nhắn 1, hãy ký một tin nhắn off-chain. - Nếu không, hãy ký cùng challenge đó bằng
solana:signMessage. Hầu hết các ví phần mềm đều hỗ trợ. - Hãy cho người dùng ví cứng không hỗ trợ tin nhắn một lựa chọn chủ động rõ ràng, chẳng hạn như công tắc "Tôi đang dùng ví cứng", để ký một giao dịch đăng nhập thay thế. Đừng mặc định đưa họ vào lối này.
Ký tin nhắn 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 hỗ trợ này nhận một UiWalletAccount, chẳng hạn như account được trả về bởi
useConnectedWallet 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, tức 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ủa bạn cùng với
địa chỉ ví.
Hãy viết challenge sao cho dễ đọc trên màn hình nhỏ. Nêu tên ứng dụng và 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à một bước 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 challenge bạn đã phát hành, sau đó 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 những người ký khác với
những gì bạn mong đợi. verifyOffchainMessageEnvelope sẽ 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 tin nhắn thô
Chữ ký signMessage bao phủ trực tiếp các byte của challenge, nên client
chỉ cần gửi chữ ký và địa chỉ ví. Hãy kiểm tra nó so với
challenge 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ác quy tắc nonce tương tự: 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 hỗ trợ 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 challenge. Ứng dụng của bạn truyền các trường có cấu trúc, chẳng hạn như
domain, statement, nonce và issuedAt, tới tính năng solana:signIn của ví.
Ví định dạng chúng thành một tin nhắn chuẩn, kết nối nếu cần,
và ký chỉ 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ẽ bao bọc văn bản SIWS
trong một tin nhắn 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í 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í
quả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 hỗ trợ.
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 hỗ trợ 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;}
Hãy 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à lưu một bản sao theo khóa là 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 cáo một tin nhắn
off-chain, signedMessage là một tin nhắn 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 của bạn và địa chỉ ký
bằng createSignInMessageText từ @solana/wallet-standard-util, sau đó xác minh nó
giống như mọi tin nhắn off-chain khác, như trong
Xác minh chữ ký trên máy chủ của bạn. Tài liệu
README của @solana/offchain-messages
bao quát 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 tin nhắn 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, sau đó sắp xếp và kiểm tra các giá trị 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 ngoài chuỗi phiên bản 1, trả về các byte của nó dưới dạngsignedMessage, và thiết lậpsignedMessageFormat. - 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ó như một giải pháp dự phòng.
Is this page helpful?