Accounts & Keys
Identity, wallet creation, PoW challenges
Every XE account is an ed25519 key pair. The public key, hex-encoded, serves as the account address. There are no separate address formats, prefixes, or checksums -- the address is the public key.
Key pair
The KeyPair struct holds both halves:
type KeyPair struct {
Public ed25519.PublicKey // 32 bytes
Private ed25519.PrivateKey // 64 bytes (seed + public key)
}Account address
The account address is the hex-encoded public key: 64 hexadecimal characters representing 32 bytes.
e5f6a7b8c9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e[!NOTE] Note Addresses are always lowercase hex. There is no base32, bech32, or other encoding -- just raw hex.
Generation
Random key pair -- used for new accounts:
kp, err := core.GenerateKeyPair()
// kp.Public = 32-byte ed25519 public key
// kp.Private = 64-byte ed25519 private key
// kp.PubKeyHex() = "e5f6a7b8..." (64 hex chars)Internally this calls crypto/ed25519.GenerateKey(crypto/rand.Reader).
Deterministic from seed -- recreates a key pair from a known 32-byte seed:
seed := make([]byte, 32)
// ... fill seed from backup, derivation, etc.
kp := core.KeyPairFromSeed(seed)This calls ed25519.NewKeyFromSeed(seed), producing the same key pair every time for the same seed. The node persists its seed in {dataDir}/node.key so the account survives restarts.
Signing
Blocks are signed with the account's private key. The process:
-
Canonical encoding -- the block is serialized into its canonical binary form via
MarshalBlockCanonical(). This is a fixed-layout binary encoding that excludesPoWNonce,Hash, andSignature. See Encoding for the full binary layout. -
Hash -- SHA-256 over the network ID, the canonical bytes, and the auxiliary tail, hex-encoded:
hash, err := core.HashBlock(b) // hash = hex(sha256(networkID || canonicalBytes || auxBytes))The network ID scopes every block to one network instance, so a block from one network can never be replayed onto another. The auxiliary tail is empty for non-lease blocks; on lease-family blocks it binds the
certificate_hashand the timekeeper attestations — fields that are attached after the body is built — into the hash. -
Sign -- ed25519 signature over the raw hash bytes (not the hex string):
err := core.SignBlock(b, kp) // Sets b.Hash and b.SignatureThe signature is computed as
ed25519.Sign(privateKey, hashBytes)wherehashBytesis the 32-byte SHA-256 digest (decoded from hex). -
Result --
b.Hashcontains the 64-character hex SHA-256 hash;b.Signaturecontains the 128-character hex ed25519 signature.
Verification
Any node can verify a block without knowing the private key:
err := core.VerifyBlock(b)This performs three checks:
- Recompute hash -- re-encode the block canonically, recompute the hash, and compare to
b.Hash. - Decode public key -- hex-decode
b.Accountinto a 32-byte ed25519 public key. - Verify signature -- call
ed25519.Verify(pubKey, hashBytes, sigBytes)to confirm the signature was produced by the account's private key.
If any step fails, the block is rejected. Multisig blocks (those carrying a signatures array) stop after the hash check here -- their signature set is validated by the ledger, which holds the account's keyset.
Account lifecycle
An account does not need to be "created" before it can receive funds. The first block on an account's chain opens the account. This can be:
- A receive block -- accepts a pending send from another account. This is how ordinary accounts are opened.
- A mint block -- creates XUSD on an authorized minter's own chain. Only accounts registered under the state chain's
sys.minterkey can do this. - A multisig_open block -- opens a multisig account with a hash-derived address.
- The genesis block -- opens the treasury account with the initial XE supply. There is exactly one, and only on an empty ledger.
Before the first block, the account has no chain, no balance, and no frontier. The account address (public key) exists as a mathematical entity -- it becomes active on the ledger only when its first block is added.
Multisig accounts
A multisig account is controlled by M-of-N keyholders instead of a single key. The account address is a hash-derived address — the SHA-256 hash of the canonical keyset encoding:
address = sha256(threshold_u32_be || num_keys_u32_be || key1_32_bytes || key2_32_bytes || ...)Keys are sorted lexicographically before encoding, so the same set of keys always produces the same address regardless of input order.
Multisig accounts are opened with a multisig_open block that registers the keyset on the ledger. The keyset can be rotated via multisig_update blocks (signed by the old keyset's threshold). The address never changes — it was derived from the original keyset.
Signature requirements
Operation
Requirement
send, lease, mint, multisig_open, multisig_update
M-of-N (threshold signatures)
every other block type
1-of-N (any single keyholder)
Spending operations require the full threshold. Everything else requires only one signature from the keyset, making multisig accounts operationally practical for incoming transfers.
Multisig blocks use a signatures array instead of the single signature field:
{
"signatures": [
{"public_key": "<pubkey1>", "signature": "<sig1>"},
{"public_key": "<pubkey3>", "signature": "<sig3>"}
]
}Delegation
Each block includes an optional Representative field -- a 64-character hex public key of the account that should vote on this account's behalf. This is how consensus weight is delegated. Only XE balances contribute to delegation weight; XUSD does not, because XUSD is mintable by authorized minters and must never mint consensus weight along with it.
If Representative is empty on a block, the existing delegation is preserved — it means "keep current delegation", not "clear delegation". On the first block, an empty representative means no delegation is set.
See Delegation for details on how representative voting works.
Key storage
The node stores its 32-byte seed in {dataDir}/node.key as raw bytes. The key pair is derived from this seed on startup. If the file does not exist, a new random seed is generated.
The xe CLI keeps a separate wallet seed at ~/.xe/wallet.seed, overridable with the XE_WALLET environment variable. The node's identity key and the CLI wallet are different accounts.
[!WARNING] Backup your seed The
node.keyfile is the only way to recover the node's account. If lost, that account's funds are permanently inaccessible. Back it up securely.