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:

  1. Canonical encoding -- the block is serialized into its canonical binary form via MarshalBlockCanonical(). This is a fixed-layout binary encoding that excludes PoWNonce, Hash, and Signature. See Encoding for the full binary layout.

  2. 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_hash and the timekeeper attestations — fields that are attached after the body is built — into the hash.

  3. Sign -- ed25519 signature over the raw hash bytes (not the hex string):

    err := core.SignBlock(b, kp)
    // Sets b.Hash and b.Signature

    The signature is computed as ed25519.Sign(privateKey, hashBytes) where hashBytes is the 32-byte SHA-256 digest (decoded from hex).

  4. Result -- b.Hash contains the 64-character hex SHA-256 hash; b.Signature contains 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:

  1. Recompute hash -- re-encode the block canonically, recompute the hash, and compare to b.Hash.
  2. Decode public key -- hex-decode b.Account into a 32-byte ed25519 public key.
  3. 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.minter key 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.key file is the only way to recover the node's account. If lost, that account's funds are permanently inaccessible. Back it up securely.