Skip to main content

Offline Signing

Offline signing is the process of signing transactions using a device that is not connected to a Sui network or using a wallet implemented in a different programming language that doesn't rely on Sui Keystore. To implement offline signing:

  1. Serialize the data for signing.

  2. Place the serialized data in a location, such as the wallet of your choice, or tools in other programming languages.

  3. Sign the data.

  4. Execute the signed transaction.

Use offline signing when the signing key lives on an air-gapped machine, or when you sign in a hardware security module (HSM), a trusted execution environment (TEE), or another environment without the Sui SDK (see How do I sign with an HSM or a TEE?). If you can sign with the active key in Sui Keystore instead, see Alternative: sign with Sui Keystore.

How do I serialize a transaction for signing elsewhere?​

You must serialize transaction data following Binary Canonical Serialization (BCS). It is supported in other languages.

The following example demonstrates how to serialize data for a transfer using the Sui CLI. This returns serialized transaction data in Base64. Submit the raw transaction to execute as tx_bytes.

tip

Beginning with the Sui v1.24.1 release, the --gas-budget option is no longer required for CLI commands.

$ sui client ptb --transfer-objects "[gas]" @<SUI-ADDRESS> --gas-coin @<COIN-OBJECT-ID> --gas-budget <GAS-AMOUNT> --serialize-unsigned-transaction

The console responds with the resulting <TX_BYTES> value.

tip

All other CLI commands that craft a transaction (such as sui client publish and sui client call) also accept the --serialize-unsigned-transaction flag and you can use it in the same way.

How do I sign the serialized data?​

This is the step you perform on the offline device or in your external signing environment.

caution

Cold-signer checklist: before you sign anything offline, decode and verify the transaction. Confirm the target network, the recipient address, the package IDs, the object IDs, the TransactionKind, and the intent bytes match what you expect. Never blind-sign opaque bytes that you cannot inspect and reconstruct. See Frontend and transaction signing for guidance.

You can sign the data using the device and programming language of your choice. Sui accepts signatures for pure Ed25519, ECDSA Secp256k1, ECDSA Secp256r1, and native multisig. To learn more about the requirements of the signatures, see Sui Signatures.

This example uses the sui keytool command to sign, using the Ed25519 key corresponding to the provided address stored in sui.keystore. This command outputs the signature, the public key, and the flag encoded in Base64. This command is backed by fastcrypto.

sui keytool sign --address <SUI-ADDRESS> --data <TX_BYTES>

You receive the following response:

Signer address: <SUI-ADDRESS>
Raw tx_bytes to execute: <TX_BYTES>
Intent: Intent { scope: TransactionData, version: V0, app_id: Sui }
Raw intent message: <INTENT-MESSAGE>
Digest to sign: <DIGEST>
Serialized signature (`flag || sig || pk` in Base64): <SERIALIZED-SIGNATURE>

To ensure the signature produced offline meets Sui validity rules for testing purposes, you can import the mnemonics to sui.keystore using sui keytool import. You can then sign with it using sui keytool sign and compare the signature results. Additionally, you can find test vectors in ~/sui/sdk/typescript/test/e2e/raw-signer.test.ts.

To verify a signature against the cryptography library backing Sui when debugging, see sigs-cli.

How do I execute the signed transaction?​

Unlike signing, this step submits the transaction to a Sui network. After you obtain the serialized signature, you can submit it using the execution transaction command. This command takes --tx-bytes as the raw transaction bytes to execute (see output of the previous sui client ptb command) and the serialized signature (Base64 encoded flag || sig || pk, see output of sui keytool sign). This executes the signed transaction and returns the certificate and transaction effects if successful.

$ sui client execute-signed-tx --tx-bytes <TX_BYTES> --signatures <SERIALIZED-SIGNATURE>

For the transfer example, you receive the following response. The Transaction Kind, Recipient, and Amount fields reflect that transfer, so your output differs for other transaction types:

----- Certificate ----
Transaction Hash: <TRANSACTION-ID>
Transaction Signature: <SIGNATURE>
Signed Authorities Bitmap: RoaringBitmap<[0, 1, 3]>
Transaction Kind : Transfer SUI
Recipient : <SUI-ADDRESS>
Amount: Full Balance

----- Transaction Effects ----
Status : Success
Mutated Objects:
- ID: <OBJECT_ID> , Owner: Account Address ( <SUI-ADDRESS> )

Alternative: sign with Sui Keystore and execute transaction​

Alternatively, you can use the active key in Sui Keystore to sign and output a Base64-encoded sender signed data with flag --serialize-signed-transaction.

$ sui client ptb --transfer-objects "[gas]" @<SUI-ADDRESS> --gas-coin @<COIN-OBJECT-ID> --gas-budget <GAS-AMOUNT> --serialize-signed-transaction

The console responds with the resulting <SIGNED-TX-BYTES> value.

After you obtain the signed transaction bytes, you can submit it using the execute-combined-signed-tx command. This command takes --signed-tx-bytes as the signed transaction bytes to execute (see output of the previous sui client ptb command). This executes the signed transaction and returns the certificate and transaction effects if successful.

$ sui client execute-combined-signed-tx --signed-tx-bytes <SIGNED-TX-BYTES>

How do I sign with an HSM or a TEE?​

If you sign transactions in a TEE, HSM, or any environment without the Sui SDK, implement the following pipeline manually:

  1. Serialize the TransactionData to BCS bytes.
  2. Prepend the 3-byte intent prefix: [0, 0, 0] for transactions (see Intent prefix reference for why the prefix exists).
  3. Hash the prefixed bytes with Blake2b-256 (producing 32 bytes).
  4. Sign the hash with your Ed25519 private key (producing a 64-byte signature).
  5. Encode the signature as flag_byte || signature_bytes || public_key_bytes, then Base64 encode the result.

The flag byte indicates the signature scheme: 0x00 for Ed25519, 0x01 for Secp256k1, 0x02 for Secp256r1. To learn more about the signature schemes Sui accepts, see Sui Signatures.

Pseudocode​

intent_prefix = bytes([0, 0, 0])
message = intent_prefix + bcs_serialize(transaction_data)
digest = blake2b256(message)
signature = ed25519_sign(private_key, digest)
encoded = base64(bytes([0x00]) + signature + public_key)

Intent prefix reference​

Intent scopePrefix bytesUsed by
Transaction[0, 0, 0]User transaction signing.
Personal message[3, 0, 0]signPersonalMessage API.
Proof of possession[5, 0, 0]Validator registration.

The prefix exists for domain separation: it prevents a signature over a personal message from being reused as a transaction signature.