Sei Network Accounts: Dual Address System Explained
Understand how Sei’s unified account system works with both EVM (0x) and Cosmos (sei1) addresses, including association methods, security considerations, and cross-environment interactions.
Use this file to discover all available pages before exploring further.
Every account on Sei has a unique public key. This public key can be used to
generate multiple wallet addresses, but it is important to note that the two are
functionally the same. They appear different, and depending on the app they may
be used interchangeably, but they both point to the same destination - your
account. The difference is like the difference between the numeral “2” and the
word “two”. They both define the same value, but may be used in different
contexts.
“hex” Address: Starts with 0x and is EVM-based.
“bech32” Address: Starts with sei1 and is used for Cosmos functions.
Although these addresses appear different, they actually share the same
underlying account. This means whatever action you take with one address will
also affect the other.If you deposit funds into your EVM address, you can access and use those same
funds with your SEI address, and vice versa. They are linked together as one
account, ensuring seamless integration between the EVM and SEI ecosystems.Both addresses for a single account are derived from the same public key, but the chain can only determine their association after the public key is known by the chain via association.
The Bech32 (sei...) and EVM (0x...) addresses are treated as separate
accounts.
They will have separate balances until linked.
Cosmos tokens received by the EVM address prior to association will be held in
a temporary Cosmos Bech32 address, which will transfer to the associated address upon
linking.
Some types of transactions will not be possible (see table below).
Using any of these methods will ensure the public key is known to the chain, enabling automatic association between the EVM-compatible and Bech32 addresses.
Constants for the addr precompile can also be found in the repo
Sei-Chain/precompiles:
The keccak256 hashing ensures a consistent and verifiable process for deriving
both address formats from the same public key. This enables a single account to
maintain compatibility across the Cosmos and EVM environments.
When deriving a private key from a mnemonic phrase, the hierarchical
deterministic (HD) path involves multiple parameters, including the coin type.
The coin type determines the blockchain ecosystem for which the key is derived,
making it crucial when dealing with different wallets and blockchains.
The second parameter in the HD path specifies the coin type, which is defined by
the BIP-44 standard. This parameter identifies the blockchain ecosystem
associated with the derived keys.
Ethereum (Coin Type 60): Wallets like MetaMask use coin type 60. The HD
path for Ethereum typically looks like this: m/44'/60'/0'/0/0.
Cosmos (Coin Type 118): Wallets for Cosmos-based chains, such as Compass,
use coin type 118. The HD path for Cosmos typically looks like this:
m/44'/118'/0'/0/0.
Due to the different coin types, a mnemonic phrase used to derive keys for
Ethereum (coin type 60) cannot be directly used in a Cosmos wallet (coin
type 118) to access the same accounts. This is because the HD path determines a
different set of keys for each coin type, meaning the derived addresses will
differ.
Users can export their private key from MetaMask (derived using coin type 60)
and import it into any Cosmos wallet. This works because the private key, once
derived, can be used across different blockchain ecosystems, provided the
receiving wallet supports the import function. This allows users to manage their
assets across various blockchains using the same underlying cryptographic key.
Sei uses a unique method of deriving both the Cosmos/Tendermint style bech32
address and the Ethereum-style hex address from the same public key, using the
keccak hashing method common in EVM networks. These extensively commented snippets demonstrate the ‘proper’ method of deriving
both bech32 and hex addresses from a given ECDSA SECP256k1 key:
Python - from pubkey
import base64import jsonfrom hashlib import sha256, new as hashlib_newfrom coincurve import PublicKeyfrom bech32 import bech32_encode, convertbitsfrom Crypto.Hash import keccak# Example input, replace with the actual pubkey JSON stringpubkey_json = '{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"Agmik4xkmF57hNjzykYHP3gRu1Mpae4B5BCiwx7jmRzI"}'# Extract the base64-encoded key from the JSON-like stringpubkey_dict = json.loads(pubkey_json)pubkey_base64 = pubkey_dict['key']# Decode the base64-encoded public keypublic_key_compressed = base64.b64decode(pubkey_base64)# Ensure the public key length is 33 bytes (compressed key format)if len(public_key_compressed) != 33: raise ValueError(f"Invalid public key length, expected 33 bytes for compressed format, got {len(public_key_compressed)}")# Debugging: Print the public key detailsprint(f"Compressed public key (hex): {public_key_compressed.hex()}")# SHA-256 on the compressed public keysha256_digest = sha256(public_key_compressed).digest()# Debugging: Print SHA-256 digestprint(f"SHA-256 Digest: {sha256_digest.hex()}")# RIPEMD-160 on SHA-256 hashripemd160 = hashlib_new('ripemd160')ripemd160.update(sha256_digest)ripemd160_digest = ripemd160.digest()# Debugging: Print RIPEMD-160 digestprint(f"RIPEMD-160 Digest: {ripemd160_digest.hex()}")# Convert the digest to 5-bit groups for Bech32 encodingfive_bit_ripemd160 = convertbits(ripemd160_digest, 8, 5, pad=True)bech32_address = bech32_encode("sei", five_bit_ripemd160)print(f"Bech32 Cosmos Address: {bech32_address}")# Decompress the public key to 65 bytespublic_key = PublicKey(public_key_compressed).format(compressed=False)# Debugging: Print the public key detailsprint(f"Decompressed public key length: {len(public_key)}")print(f"Decompressed public key (hex): {public_key.hex()}")# Derive Ethereum Address using Keccak-256keccak_hash = keccak.new(digest_bits=256)keccak_hash.update(public_key[1:]) # Exclude the first byte (0x04)digest_keccak = keccak_hash.digest()eth_address = digest_keccak[-20:]eth_address_hex = '0x' + eth_address.hex()print(f"Ethereum Address: {eth_address_hex}")
Typescript - from pubkey
import { fromBase64 } from '@cosmjs/encoding';import { sha256 } from '@noble/hashes/sha256';import { ripemd160 } from '@noble/hashes/ripemd160';import { keccak_256 } from '@noble/hashes/sha3';import { secp256k1 } from '@noble/curves/secp256k1';import { bech32 } from 'bech32';// Utility function to convert bits for Bech32 encodingfunction convertBits(data: Uint8Array, fromBits: number, toBits: number, pad: boolean): number[] { let acc = 0; let bits = 0; const result: number[] = []; const maxv = (1 << toBits) - 1; for (const value of data) { acc = (acc << fromBits) | value; bits += fromBits; while (bits >= toBits) { bits -= toBits; result.push((acc >> bits) & maxv); } } if (pad) { if (bits > 0) { result.push((acc << (toBits - bits)) & maxv); } } else if (bits >= fromBits || (acc << (toBits - bits)) & maxv) { throw new Error('Unable to convert bits'); } return result;}// Define the prefix for the Bech32 address (e.g., "sei" for Sei network)const chainPrefix = 'sei';// Public key JSON string (replace with actual data)const pubkeyJson = '{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"AiN+aFvHgjblWPaP9Er5p005JjPX3nj4I/+jA6W4BOho"}';// Parse the JSON string to extract the public key in Base64 formatconst pubkeyDict = JSON.parse(pubkeyJson);const pubkeyBase64 = pubkeyDict.key;console.log('Original public key JSON:', pubkeyJson);console.log('Parsed public key object:', pubkeyDict);console.log('Base64-encoded public key:', pubkeyBase64);// Decode the Base64-encoded public key to its compressed formconst publicKeyCompressed = fromBase64(pubkeyBase64);console.log('Compressed public key (bytes):', publicKeyCompressed);console.log('Compressed public key (hex):', Buffer.from(publicKeyCompressed).toString('hex'));// Perform SHA-256 hashing on the compressed public keyconst sha256Digest = sha256(publicKeyCompressed);console.log('SHA-256 hash of public key (hex):', Buffer.from(sha256Digest).toString('hex'));// Perform RIPEMD-160 hashing on the SHA-256 digestconst ripemd160Digest = ripemd160(sha256Digest);console.log('RIPEMD-160 hash of SHA-256 hash (hex):', Buffer.from(ripemd160Digest).toString('hex'));// Convert the RIPEMD-160 digest to a 5-bit array for Bech32 encodingconst fiveBitArray = convertBits(ripemd160Digest, 8, 5, true);// Encode the 5-bit array into a Bech32 address with the specified prefixconst bech32Address = bech32.encode(chainPrefix, fiveBitArray);console.log(`Bech32 Cosmos Address: ${bech32Address}`);// Decompress the public key to its uncompressed form (65 bytes) and exclude the first byteconst publicKeyUncompressed = secp256k1.ProjectivePoint.fromHex(publicKeyCompressed).toRawBytes(false).slice(1);// Perform Keccak-256 hashing on the uncompressed public key to derive the Ethereum addressconst keccakHash = keccak_256(publicKeyUncompressed);const ethAddress = '0x' + Buffer.from(keccakHash.slice(-20)).toString('hex');console.log('Uncompressed public key (hex):', Buffer.from(publicKeyUncompressed).toString('hex'));console.log('Keccak-256 hash of uncompressed public key (hex):', Buffer.from(keccakHash).toString('hex'));console.log(`Ethereum Address: ${ethAddress}`);
Typescript - Full Derivation from Private Key
import { sha256 } from '@noble/hashes/sha256';import { ripemd160 } from '@noble/hashes/ripemd160';import { keccak_256 } from '@noble/hashes/sha3';import { secp256k1 } from '@noble/curves/secp256k1';import { bech32 } from 'bech32';// Utility function to convert bits for Bech32 encodingfunction convertBits(data: Uint8Array, fromBits: number, toBits: number, pad: boolean): number[] { let acc = 0; let bits = 0; const result: number[] = []; const maxv = (1 << toBits) - 1; for (const value of data) { acc = (acc << fromBits) | value; bits += fromBits; while (bits >= toBits) { bits -= toBits; result.push((acc >> bits) & maxv); } } if (pad) { if (bits > 0) { result.push((acc << (toBits - bits)) & maxv); } } else if (bits >= fromBits || (acc << (toBits - bits)) & maxv) { throw new Error('Unable to convert bits'); } return result;}// Function to generate addresses from a private keyfunction generateAddresses(privateKeyHex: string): { seiAddress: string; ethAddress: string;} { // Ensure the private key is exactly 32 bytes long const privateKey = Uint8Array.from(Buffer.from(privateKeyHex.padStart(64, '0'), 'hex')); if (privateKey.length !== 32) { throw new Error('Private key must be 32 bytes long.'); } // Derive the compressed public key from the private key const publicKey = secp256k1.getPublicKey(privateKey, true); const publicKeyBytes = publicKey; // Perform SHA-256 hashing on the compressed public key const sha256Digest = sha256(publicKeyBytes); // Perform RIPEMD-160 hashing on the SHA-256 digest const ripemd160Digest = ripemd160(sha256Digest); // Convert the RIPEMD-160 digest to a 5-bit array for Bech32 encoding const fiveBitArray = convertBits(ripemd160Digest, 8, 5, true); // Bech32 address with "sei" prefix const seiAddress = bech32.encode('sei', fiveBitArray, 256); // Derive the uncompressed public key from the private key and exclude the first byte const publicKeyUncompressed = secp256k1.getPublicKey(privateKey, false).slice(1); // Perform Keccak-256 hashing on the uncompressed public key to derive the Ethereum address const keccakHash = keccak_256(publicKeyUncompressed); const ethAddress = `0x${Buffer.from(keccakHash).slice(-20).toString('hex')}`; return { seiAddress, ethAddress };}// Example usage of the generateAddresses functionconst privateKeyHex = '907ab4bf7fc60cff';const { seiAddress, ethAddress } = generateAddresses(privateKeyHex);console.log(`Sei Address: ${seiAddress}`);console.log(`Ethereum Address: ${ethAddress}`);