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:

FunctionDescription
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: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2

There 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:

AlgorithmUseDigest size
SHA-256Block, state chain block, chat envelope, attestation and directory-registration hashing32 bytes
Blake2bProof of work8 bytes (truncated)

Block hashing

The block hash is SHA-256(networkID || canonical || aux):

  • networkID is 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.
  • canonical is MarshalBlockCanonical(block).
  • aux is MarshalBlockAux(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:

GoBrowserPurpose
crypto/ed25519Web Crypto Ed25519 (subtle.importKey / subtle.sign)Key derivation from seed, signing
crypto/sha256Web Crypto API (subtle.digest)Block hashing
golang.org/x/crypto/blake2bBundled assets/blake2b.jsProof 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