Skip to main content

Off-chain message signing

There is ecosystem demand for a method of signing non-transaction messages with a Solana wallet. Typically this is some kind of "proof of wallet ownership" for entry into a whitelisted system.

This document specifies version 1 of the off-chain message format, the current recommended format. The original version 0 format remains supported and is documented in Appendix: version 0 (legacy).

Some inspiration can be gleaned from relevant portions of Ethereum's EIP-712.

Motivation

  • Security
    • Off-chain message signatures must not be valid transaction message signatures
  • Future-proofing
    • Versioning
    • Co-exist with versioned transaction messages and extended length transactions
  • Compatibility
    • Hardware wallet signing
    • Determinism
      • The same logical message must always produce the same bytes, and therefore the same signature, regardless of the order in which signers are declared

What changed in version 1

Version 1 is a deliberate simplification of version 0. It removes three fields that added complexity or ambiguity without proportional benefit:

  • Application domain (32 bytes) — provided limited utility and created a spoofing surface. Removed.
  • Message format (1 byte) — whether a given message is signable or clear-signable by a hardware wallet depends on that wallet's implementation, not on a byte in the message. Removed; clients may impose their own character set or length constraints out of band.
  • Message length (2-byte prefix) — the 16-bit length prefix capped messages at 65,535 bytes and created a class of length-mismatch validation failures. Removed; the message content is now a trailing, variable-length field that consumes the remainder of the buffer.

Version 1 also adds an ordering and uniqueness constraint on signer addresses so that identical messages always serialize identically (see Required signers).

Message format

A version 1 off-chain message is the byte string that is ed25519-signed. It is composed of a preamble followed by the message content.

FieldStart offsetLength (bytes)Description
Signing domain0x0016[1]
Version0x101An 8-bit unsigned integer. MUST equal 1. [2]
Signer count0x111Number of required signers. An 8-bit unsigned integer. MUST NOT be zero.
Signers0x12SIGNER_COUNT * 32SIGNER_COUNT ed25519 public keys. [3]
Content0x12 + SIGNER_COUNT*32remainder of bufferThe message content. UTF-8. MUST NOT be empty. [4]

Signing domain

The signing domain is a fixed 16-byte prefix that gives common structure to all off-chain message signatures:

b"\xffsolana offchain"

In hexadecimal: ff 73 6f 6c 61 6e 61 20 6f 66 66 63 68 61 69 6e.

The first byte, \xff, was chosen for the following reasons:

  1. It corresponds to a value that is illegal as the first byte in a transaction MessageHeader (see Runtime considerations).
  2. It avoids unintentional misuse in languages with C-like, null-terminated strings.

The remaining bytes, b"solana offchain", were chosen to be descriptive and reasonably long, but are otherwise arbitrary.

This field SHOULD NOT be displayed to users.

Version

The version is an 8-bit unsigned integer. For messages conforming to this document it MUST equal 1.

This field SHOULD NOT be displayed to users.

Required signers

The list of SIGNER_COUNT ed25519 public keys that MUST sign the message for it to be valid. The list:

  • MUST NOT be empty.
  • MUST NOT contain duplicates.
  • MUST be sorted in ascending lexicographical (byte-wise) order.

The ordering and uniqueness constraints exist so that a given logical message always serializes to the same bytes — and therefore the same signature — no matter the order in which an application declares its signers. Signers and verifiers MUST enforce both constraints.

These addresses SHOULD be displayed to users as base58-encoded strings.

Content

The message content occupies the remainder of the buffer after the signers. It is a UTF-8 encoded string and MUST NOT be empty. Unlike version 0, there is no length prefix and no upper bound imposed by the format itself.

Hardware-wallet signability and clear-signability are properties of the signing device, not of the message format. Clients that need to target a specific hardware wallet MAY impose their own additional restrictions — for example limiting content to printable ASCII (0x20..=0x7e) or to a maximum byte length — but the format does not require or encode these choices.

Signing

Solana off-chain messages MUST only be signed using the ed25519 digital signature scheme. Before signing, the message MUST be strictly checked to conform to this specification, including the signer ordering and uniqueness constraints. The full message byte string (signing domain, version, signers, and content) is then ed25519-signed by each required signer.

Verification

A message is valid only if, for every required signer in the preamble, a corresponding ed25519 signature is present and verifies against the full message byte string. The message MUST also be strictly checked to conform to this specification — a message that does not conform is invalid regardless of the validity of any signatures.

Envelope

When passing around signed off-chain messages a common format is helpful. The recommended binary representation is as follows:

FieldStart offsetLength (bytes)Description
Signature count0x001Number of signatures. An 8-bit unsigned integer. MUST NOT be zero.
Signatures0x01SIG_COUNT * 64SIG_COUNT ed25519 signatures.
Message0x01 + SIG_COUNT * 64remainderThe signed message (preamble and content).
  • The signature count MUST equal the signer count from the message preamble.
  • Signatures MUST be ordered to match their corresponding public keys as they appear in the (sorted) signers list.
  • A missing signature in a partially-signed envelope is represented by 64 zero bytes.

Runtime considerations

To prevent social attacks by which the signer is tricked into signing a transaction, the runtime MUST NOT accept signed off-chain messages as transactions under any circumstances. The first byte of the signing domain is chosen such that it corresponds to a value (0xff) which is implicitly illegal as the first byte in a transaction MessageHeader today. The property is implicit because the top bit in the first byte of a MessageHeader being set signals a versioned transaction, but only a value of zero is supported at this time. The runtime reserves 127 as an illegal version number, making this property explicit.

Reference

  • Specification discussion: SRFC #3
  • Offchain messages SDK: @solana/offchain-messages (exported via @solana/kit)
  • The runtime check that reserves transaction version 127 (rejecting 0xff-prefixed bytes as transactions) was implemented in PR #29807

Appendix: version 0 (legacy)

Version 0 is the original off-chain message format. It remains supported for backwards compatibility, but new applications SHOULD prefer version 1. The differences are summarized in What changed in version 1.

Message preamble (v0)

FieldStart offsetLength (bytes)Description
Signing Domain0x0016Same as v1.
Header version0x101An 8-bit unsigned integer. 0 for this format.
Application domain0x1132A 32-byte application identifier (e.g. a program address, DAO, etc.). [1]
Message format0x311[2]
Signer count0x321Number of signers. An 8-bit unsigned integer. MUST NOT be zero.
Signers0x33SIGNER_COUNT * 32SIGNER_COUNT ed25519 public keys of signers.
Message length0x33 + SIGNER_CNT * 322Length of message in bytes. A 16-bit, unsigned, little-endian integer. MUST NOT be zero.

Application domain (v0)

A 32-byte array identifying the application requesting off-chain message signing. This may be any arbitrary bytes. This field SHOULD be displayed to users as a base58-encoded ASCII string rather than interpreted otherwise.

Message format (v0)

Version 0 headers specify three message formats allowing for trade-offs between compatibility and composition of messages.

IDEncodingMaximum Length *Hardware Wallet Support
0Restricted ASCII **1232Yes
1UTF-81232Blind sign only
2UTF-865535No

* Combined length of the message preamble and message body
** Those characters for which isprint(3) returns true. That is, 0x20..=0x7e.

Both the message encoding and maximum message length MUST be enforced by signer and verifier. Formats 0 and 1 are motivated by hardware wallet support where both RAM to store the payload and font character support are limited.

Envelope (v0)

FieldStart offsetLength (bytes)Description
Signature Count0x001Number of signatures. An 8-bit, unsigned integer.
Signatures0x01SIG_COUNT * 64SIG_COUNT ed25519 signatures.
Message Preamble0x01 + SIG_COUNT * 64PREAMBLE_LENThe message preamble.
Message Body0x01 + SIG_COUNT * 64 + PREAMBLE_LENMESSAGE_LENThe message content.

The signature count MUST match the signer count from the message preamble, and signatures MUST be ordered to match their corresponding public keys.