Skip to main content

Intent Signing

An intent is a compact struct that serves as the domain separator for a message that a signature commits to. The data that the signature commits to is an intent message. All user signatures in Sui must commit to an intent message rather than the message itself.

The intent signing standard provides a compact domain separator to the data being signed for both user signatures and authority signatures. It has several benefits:

  • A u8 representation replaces the Rust struct tag name string for the intent scope.

  • In addition to the intent scope, you can commit other important domain separators as well (such as intent version and app ID).

  • The data itself no longer needs to implement the Signable trait, it just needs to implement Serialize.

  • All signatures can adopt the same intent message structure, including both user signatures (only to commit to TransactionData) and authority signature (commits to all internal intent scopes such as TransactionEffects, ProofOfPossession, and SenderSignedTransaction).

Structs​

The IntentMessage struct consists of the intent and the serialized data value.

pub struct IntentMessage<T> {
pub intent: Intent,
pub value: T,
}

To create an intent struct, include the IntentScope (type of message), IntentVersion (version the network supports), and AppId (application the signature refers to).

pub struct Intent {
scope: IntentScope,
version: IntentVersion,
app_id: AppId,
}

For detailed field definitions, see the enum definitions in the source code:

pub enum IntentScope {
TransactionData = 0, // Used for a user signature on a transaction data.
TransactionEffects = 1, // Used for an authority signature on transaction effects.
CheckpointSummary = 2, // Used for an authority signature on a checkpoint summary.
PersonalMessage = 3, // Used for a user signature on a personal message.
SenderSignedTransaction = 4, // Used for an authority signature on a user signed transaction.
ProofOfPossession = 5, // Used as a signature representing an authority's proof of possession of its authority protocol key.
HeaderDigest = 6, // Used for narwhal authority signature on header digest.
BridgeEventUnused = 7, // for bridge purposes but it's currently not included in messages.
ConsensusBlock = 8, // Used for consensus authority signature on block's digest.
DiscoveryPeers = 9, // Used for reporting peer addresses in discovery.
}
pub enum IntentVersion {
V0 = 0,
}
pub enum AppId {
#[default]
Sui = 0,
Narwhal = 1,
Consensus = 2,
}
pub enum HashingIntentScope {
ChildObjectId = 0xf0,
RegularObjectId = 0xf1,
}

If the definitions above do not render, see the enums directly in crates/shared-crypto/src/intent.rs. Serialized intents that appear on this page include [3, 0, 0] for a PersonalMessage intent and [5, 0, 0] for a ProofOfPossession intent, both with version V0 and app ID Sui.

The serialization of an Intent is a 3-byte array where 1 byte represents each field.

The serialization of an IntentMessage<T> is the 3 bytes of the intent concatenated with the BCS (Binary Canonical Serialization) serialized message.

How are user signatures created?​

To create a user signature, construct an intent message first, then sign the 32-byte Blake2b-256 hash of its BCS serialization (hash(intent || message)). The signer does not sign the raw transaction bytes. It signs the hash of the structured IntentMessage, whose BCS serialization is the 3-byte intent followed by the BCS bytes of the transaction data. The signed digest therefore commits to both the intent and the message.

The following examples demonstrate this:

let intent = Intent::default();
let intent_msg = IntentMessage::new(intent, data);
let signature = Signature::new_secure(&intent_msg, signer);

The TypeScript example is an excerpt from a signer class method, so this refers to the signer instance.

Internally, the new_secure method in Rust and the signData method in TypeScript do the following:

  1. Serialize the intent message as the 3-byte intent concatenated with the BCS serialized bytes of the transaction data.

  2. Apply Blake2b hash to get the 32-byte digest.

  3. Pass the digest to the signing API for each corresponding scheme of the signer. The supported signature schemes are pure Ed25519, ECDSA Secp256k1, and ECDSA Secp256r1. See Sui Signatures for requirements of each scheme.

How are authority signatures created?​

The authority signature uses the protocol key. Unlike a user signature, it does not use the external Blake2b-256 prehash that user signatures use. Instead, AuthoritySignature::new_secure does the following:

  1. BCS-serialize the IntentMessage.

  2. Append the 8-byte epoch.

  3. Pass the resulting BCS(IntentMessage) || epoch bytes directly to the BLS signer.

In other words, the authority signs intent || message || epoch. Verification reconstructs the same bytes from the IntentMessage and the epoch, then checks them against the authority protocol public key, so every authority signature binds the epoch.

Authority signatures use the internal intent scopes, such as TransactionEffects, ProofOfPossession, and SenderSignedTransaction. For all available intent scopes, see the IntentScope enum in crates/shared-crypto/src/intent.rs.

How do I generate a proof of possession for an authority?​

When an authority requests to join the network, it must provide the protocol public key and its proof of possession (PoP). The network requires PoP to prevent rogue key attack.

The proof of possession is a BLS signature created using the authority protocol private key, committed over the following message: intent || pubkey || address || epoch. The values are as follows:

  • intent: Serialized to [5, 0, 0] representing an intent with scope as Proof of Possession, version as V0, and app_id as Sui.

  • pubkey: The serialized public key bytes of the authority BLS protocol key.

  • address: The account address associated with the authority account key.

  • epoch: Serialized to [0, 0, 0, 0, 0, 0, 0, 0].

To generate a proof of possession in Rust, see generate_proof_of_possession in crates/sui-types/src/crypto.rs. That function builds the intent message with Intent::sui_app(IntentScope::ProofOfPossession), sets the message to the protocol public key bytes concatenated with the authority account address, and signs it with the authority protocol key. For test vectors, see fn test_proof_of_possession.

How do I verify a signature in Move?​

You can verify a personal-message signature onchain using the sui::ed25519 or sui::ecdsa_k1 module. ed25519::ed25519_verify checks the signature against exactly the bytes you pass as message. The client signs the 32-byte Blake2b-256 digest of the BCS-serialized IntentMessage, not the raw message. To verify, reconstruct the intent message as the PersonalMessage intent prefix [3, 0, 0] followed by the BCS-serialized message, hash the result with Blake2b-256, and pass the 32-byte digest to ed25519_verify.

In the following example, the message argument must be that 32-byte digest, not the raw message:

use sui::ed25519;

public fun verify_personal_message(
signature: vector<u8>,
public_key: vector<u8>,
message: vector<u8>,
) {
// The client signs: Blake2b-256(BCS(IntentMessage { intent: Intent::personal_message(), value: PersonalMessage { message } }))
// The intent prefix for PersonalMessage is [3, 0, 0]
assert!(
ed25519::ed25519_verify(&signature, &public_key, &message),
0
);
}

On the client side (TypeScript), produce the signature:

const { signature } = await wallet.signPersonalMessage({
message: new TextEncoder().encode('Hello from Sui'),
});
tip

When you sign application or offchain messages (for example, login challenges or authorization payloads), structure the message so no one can replay or misuse it in another context. Include a domain or app identifier, a chain or package identifier, the explicit purpose of the message, a nonce or request ID, and a timestamp or maximum age so the verifier can reject stale or replayed messages. See Frontend and transaction signing for details.

info

Sui transaction intent signing is the cryptographic envelope format. For cross-chain bridge integrations, see the Wormhole or Axelar documentation.