Cryptography
Cryptography
The XE network uses a small set of well-established cryptographic primitives. All implementations -- Go node, xe CLI, embedded web UI -- must produce identical outputs for the same inputs.
Key generation
Algorithm: Ed25519
Two modes of key generation:
| Function | Description |
|---|---|
GenerateKeyPair() | Generate a random ed25519 keypair using crypto/rand |
KeyPairFromSeed(seed) | Deterministic derivation from a 32-byte seed. Panics on invalid seed length. |
TryKeyPairFromSeed(seed) | Like KeyPairFromSeed but returns an error instead of panicking for invalid seed length. Use for untrusted input. |
KeyPairFromSeed always produces the same keypair for a given seed. This is how the xe CLI and the web wallet derive keys -- the user stores only the seed.
Account addresses
An account address is the hex-encoded ed25519 public key -- 64 hexadecimal characters (32 bytes).
Example: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2There is no checksum, prefix, or other encoding. The raw public key bytes in hex are the address.
Hashing
Two hash functions are used for different purposes:
| Algorithm | Use | Digest size |
|---|---|---|
| SHA-256 | Block, state chain block, chat envelope, attestation and directory-registration hashing | 32 bytes |
| Blake2b | Proof of work | 8 bytes (truncated) |
Block hashing
The block hash is SHA-256(networkID || canonical || aux):
networkIDis the network identifier configured at startup (SetNetworkID). It scopes a block to one network incarnation, so a block from one network can never be replayed on another. When unset it contributes nothing.canonicalisMarshalBlockCanonical(block).auxisMarshalBlockAux(block)-- the certificate hash and timekeeper attestations, and only for lease-family blocks. It is empty for every other type.
The hash is hex-encoded for storage and display.
[!IMPORTANT] Important The canonical encoding excludes the PoW nonce. The hash covers only the content that the account holder signs. See Binary Encoding for the exact byte layout.
[!NOTE] Genesis is hashed without a network ID Genesis blocks are created before the network ID exists, so they are signed and verified with the network prefix cleared.
Signing
Block signing
Ed25519 signatures over the 32 raw bytes of the block hash.
hash = HashBlock(block) // hex-encoded SHA-256
signature = ed25519.Sign(privateKey, hex_decode(hash))SignBlock computes the hash, sets it on the block, and signs it. VerifyBlock recomputes the hash from the block fields, checks it matches, and verifies the signature against the account address (hex-decoded to the public key). Multisig blocks -- those carrying a Signatures array -- have only their hash verified here; keyset signature checking is done by the ledger, which holds the keyset.
Vote signing
Representatives sign votes over the 138-byte canonical vote signing payload -- the vote's binary encoding up to and including the final flag, excluding the signature length and signature:
payload = voteSigningBytes(vote) // 138 bytes
signature = ed25519.Sign(repPrivateKey, payload)See Binary Encoding for the vote wire format.
Attestation signing
Timekeepers sign lease attestations over a SHA-256 hash:
payload = SHA-256(hex_decode(leaseHash) || uint64_be(timestamp))
signature = ed25519.Sign(timekeeperKey, payload)The timestamp is the attestation timestamp as an 8-byte big-endian value, not a string.
Chat signing
A chat envelope's ID is SHA-256 over a length-prefixed encoding of
from, to, message (each with a 4-byte big-endian length prefix) followed by an
8-byte big-endian timestamp. The sender signs the raw 32 bytes of that ID:
id = SHA-256(len(from)||from || len(to)||to || len(message)||message || uint64_be(timestamp))
signature = ed25519.Sign(privateKey, hex_decode(id))The anti-spam proof of work is solved over the same ID bytes, binding the work to the identity it prices.
Directory signing
Account directory registrations are signed over a SHA-256 hash of a domain-separated, length-prefixed payload:
msg = "xe/directory-registration/v1\x00"
|| len||networkID || len||account || len||nodePeer || len||timestamp
signature = ed25519.Sign(privateKey, SHA-256(msg))Each field carries an 8-byte big-endian length prefix so the concatenation is unambiguous, and the network ID scopes the signature to one network.
State chain signing
DAO state chain blocks are signed over the raw 32 bytes of the block hash, which is the SHA-256 of the block's canonical encoding:
hash = SHA-256(MarshalCanonical(block))
signature = ed25519.Sign(memberKey, hex_decode(hash))Multiple DAO members sign the same hash to reach the signing threshold.
Browser implementation
The embedded web UI uses browser-native primitives that must produce byte-identical results:
| Go | Browser | Purpose |
|---|---|---|
crypto/ed25519 | Web Crypto Ed25519 (subtle.importKey / subtle.sign) | Key derivation from seed, signing |
crypto/sha256 | Web Crypto API (subtle.digest) | Block hashing |
golang.org/x/crypto/blake2b | Bundled assets/blake2b.js | Proof of work |
The seed is imported as a PKCS#8 Ed25519 private key, from which the public key -- and therefore the address -- is exported. Web Crypto Ed25519 requires a recent browser (Chrome 113+, Firefox 130+, Safari 17+).
[!WARNING] Cross-implementation compatibility The canonical encoding must match byte-for-byte between Go and JavaScript. A single byte difference in the canonical form produces a completely different hash and an invalid signature. Both implementations use version byte
0x02, identical type bytes, and identical field ordering.
Summary
┌─────────────────────────────────────────────┐
│ Cryptographic Flow │
├─────────────────────────────────────────────┤
│ │
│ seed (32 bytes) │
│ │ │
│ ▼ │
│ KeyPairFromSeed(seed) │
│ ├── publicKey (32 bytes) = address │
│ └── privateKey (64 bytes) │
│ │
│ Block signing: │
│ canonical = MarshalBlockCanonical(block) │
│ aux = MarshalBlockAux(block) │
│ hash = SHA-256(netID ‖ canonical ‖ aux) │
│ sig = ed25519.Sign(privateKey, hash) │
│ │
│ Block verification: │
│ hash = HashBlock(b), must equal b.Hash │
│ ok = ed25519.Verify(pubKey, hash, sig) │
│ │
│ Proof of work: │
│ result = Blake2b_8(nonce_LE ‖ hash) │
│ valid = result >= difficulty │
│ │
└─────────────────────────────────────────────┘See also
- Binary Encoding -- canonical byte layout for hashing
- Proof of Work -- Blake2b PoW algorithm
- Wallet Security -- AES-GCM seed encryption in the browser
- Accounts and Keys -- account model and key concepts