Accounts and Keys¶
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.
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 of the canonical bytes, hex-encoded:
-
Sign -- ed25519 signature over the raw hash bytes (not the hex string):
The 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:
This performs three checks:
- Recompute hash -- re-encode the block canonically, compute SHA-256, 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.
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 claim block -- claims 100 XUSD from the faucet.
- A receive block -- accepts a pending send from another account.
- A multisig_open block -- opens a multisig account with a hash-derived address.
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:
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, multisig_open, multisig_update |
M-of-N (threshold signatures) |
receive, claim |
1-of-N (any single keyholder) |
Spending operations require full threshold. Additive operations (receiving funds) require only one signature, 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 XUSD balances contribute to delegation weight; XE balances do not.
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.
Backup your seed
The key.seed file is the only way to recover your account. If lost, the account's funds are permanently inaccessible. Back it up securely.