Blockchains

Sui

ed25519 or secp256k1 on one chain. Blake2b over a flag byte and the public key.
IDsui17 / 23SLIP-44 784'
Chain

Sui

ed25519secp256k1

  • 0x hex (Blake2b)
Formats
1
Coin type
784'
Key 1
0xd0c2c91e…cd63af

Access

Loadblockchains.sui()()
Import@agntn/keys/blockchains/sui
Walkkeyspace in the browser

Load it

js
import { useBlockchain, blockchains } from "@agntn/keys";

const suiChain = useBlockchain(await blockchains.sui()());
suiChain.curve; // ['ed25519', 'secp256k1']

Two curves, one argument

Sui accepts several signature schemes and tags each key with a flag byte. The driver supports two of them:

SchemeFlagPublic key
ed25519 (default)0x0032 bytes
secp256k10x0133 bytes, compressed

secp256r1 (0x02) and multisig (0x03) aren't implemented. Pass one of them, or any other unknown scheme, and you get a RangeError. The driver won't quietly key or sign on ed25519 instead.

The address is Blake2b-256(flag + publicKey) with 0x in front. Because the flag is part of the hash, the same private key gives two unrelated addresses depending on the scheme.

js
// ed25519, the default
const wallet = suiChain.generateWallet();

// secp256k1: the address type doubles as the scheme
const walletSecp = suiChain.generateWallet({}, "secp256k1");

Passing "secp256k1" as the address type sets the scheme for the key as well, so generateWallet, deriveWallet, and deriveHDWallet all stay consistent with one argument. When you go step by step you have to say it twice:

js
const secpKey = suiChain.getKeyPublic(privateKey, { scheme: "secp256k1" });
const secpAddress = suiChain.getAddress(secpKey, "secp256k1");

Forget the second one and getAddress throws. The default flag is ed25519, and a 33 byte secp256k1 key isn't 32 bytes. Good thing, too. Hashed under the wrong flag, that key gives an address that validates, looks fine, and no key can ever spend from. The same check catches an ed25519 key passed as secp256k1. An uncompressed secp256k1 key is fine, it gets compressed first, because the compressed form is the only one Sui hashes.

Validation

js
suiChain.validateAddress("0x7e08ac7940568c91564ddc6f5f3bf91b15a9334194ab7855daeac51c5cc74936"); // true
suiChain.validateAddress("7e08ac7940568c91564ddc6f5f3bf91b15a9334194ab7855daeac51c5cc74936"); // false

0x and 64 hex characters, any case. No checksum, same caveat as Aptos.

Mnemonics

ed25519 goes through SLIP-10 and needs every segment hardened, m/44'/784'/0'/0'/0'. secp256k1 goes through BIP32 and takes the usual m/54'/784'/0'/0/0 shape. Pass the scheme as the address type and the driver picks the right derivation:

js
suiChain.deriveHDWallet(mnemonic, "m/44'/784'/0'/0'/0'").address; // ed25519
suiChain.deriveHDWallet(mnemonic, "m/54'/784'/0'/0/0", {}, "secp256k1").address;

Signing

signMessage signs what the Sui SDK's signPersonalMessage signs: the message as a BCS byte vector behind the PersonalMessage intent (0x03 0x00 0x00), hashed with Blake2b-256. ed25519 signs that digest as is, secp256k1 signs its SHA-256 the way Secp256k1Keypair does, so the same key gives the same bytes here and there. The scheme comes from the options, ed25519 by default.

js
const signature = suiChain.signMessage("hello", privateKey);
suiChain.verifyMessage("hello", signature, publicKey); // true

const secpSignature = suiChain.signMessage("hello", privateKey, { scheme: "secp256k1" });
suiChain.verifyMessage("hello", secpSignature, secpKey, { scheme: "secp256k1" }); // true

What comes back is the 64 signature bytes in hex, r||s without a recovery byte on secp256k1, no flag and no public key. { recovered: true } throws on both schemes, Sui carries the public key in the serialized signature and never recovers it from a v. Sui tooling wants the serialized form, flag || signature || publicKey in base64, so build that before you hand it to verifyPersonalMessageSignature:

js
import { toBase64 } from "@mysten/sui/utils";
import { verifyPersonalMessageSignature } from "@mysten/sui/verify";

const flag = 0x00; // 0x01 for secp256k1
const serialized = toBase64(
  Uint8Array.from([flag, ...Uint8Array.fromHex(signature), ...Uint8Array.fromHex(publicKey)]),
);
await verifyPersonalMessageSignature(new TextEncoder().encode("hello"), serialized, { address });