X-Change Guide

Blockchain basics, from zero

Before looking at the code, we need five ideas. If you understand these, the rest of the guide is just "where each idea lives in the project". No prior blockchain knowledge is assumed.

1 · Hash functions

A hash function takes any input (text, file, anything) and produces a fixed-length "fingerprint" — for SHA-256, always 64 hex characters. Two properties matter here:

  • Deterministic: same input always gives the same hash.
  • Avalanche effect: change one bit of the input and the hash changes completely.
  • One-way: you can't go from hash back to input (brute force only).
Live: watch a hash change

SHA-256 of your text. Any change in the input — even one letter — produces a completely different 64-character hex string. That's the property that makes hashes so useful: you can't reverse them, and any tampering is instantly visible.

241196298b4f8f7c2c632f1b844f949e3e6988bca0a6d93d0eb37b0f090b4557

X-Change uses hashes everywhere: transaction hashes, block hashes, Merkle roots, address derivation, and the proposer lottery. Each is just SHA-256 (or SHA3-256) of a string.

2 · Public-key cryptography (ed25519)

A keypair is two linked values:

  • Secret key — a random 32-byte number you keep private. It can sign messages.
  • Public key — derived from the secret key. Anyone can use it to verify a signature, but cannot forge one.

In X-Change every user has an ed25519 keypair. Their blockchain address is derived from the public key:

apps/web/lib/crypto-utils.ts — address derivation
1// address = "0x" + hex(sha3-256(pubkey)[..20])
2export function deriveAddress(rawPubkey: Uint8Array): string {
3 return "0x" + bytesToHex(sha3(rawPubkey).subarray(0, 20));
4}
Live: sign and verify with ed25519

This is the same ed25519 signing the app uses. The secret key signs; anyone with the public key can verify — but cannot forge.

address 0x96e40c76…c63d
public key 42bc485fad1e8fea0f1f6b5083f37d13b097e24bae1674d4a11a25468cea1685
secret key 62785afecadab41ae76be17a3e63cd3df98f752f14f1f7fcc0d67f1721943b79

The address is 0x + sha3-256(pubkey)[..20]. Because the address is derived from the public key, proving you own the key proves you are the address. That is the whole idea behind sign-in-with-wallet.

Why "0x…" and 42 characters?
The public key is 32 bytes. Hashing it with SHA3-256 and keeping the first 20 bytes gives a shorter, friendly identifier — like a phone number for a key. 42 chars = "0x" + 40 hex digits — this exact format is checked by is_valid_address in the node.

3 · Blocks and the chain

A block is a bundle of transactions plus some metadata. Each block stores the hash of the previous block — that link is what makes it a chain and why it's nearly impossible to rewrite history.

In X-Change a block looks like this:

apps/node/src/chain/blockchain.rs
1pub struct Block {
2 pub index: i64, // block number, starts at 0 (genesis)
3 pub hash: String, // sha256 of the fields below
4 pub prev_hash: String, // hash of the previous block <-- THE LINK
5 pub merkle_root: String, // one hash summarising all tx hashes
6 pub proposer: String, // who produced this block
7 pub tx_hashes: Vec<String>,
8 pub created_at: i64, // unix milliseconds
9}

A new block is produced every 3 seconds on a timer. Its hash is:

compute_hash
1fn compute_hash(&self) -> String {
2 let raw = format!(
3 "{}|{}|{}|{}|{}|{}",
4 self.index, self.prev_hash, self.merkle_root,
5 self.proposer, self.tx_hashes.join(","), self.created_at
6 );
7 sha256hex(raw.as_bytes())
8}

Merkle trees: summarise a list of transactions

Instead of storing every transaction hash separately, blocks store one Merkle root — hashes are combined in pairs, level by level, until one hash remains. If any transaction is altered, the root changes and the block is invalid.

Live: build a Merkle tree

In a block, the list of transactions is summarised into a single 64-char Merkle root. Hashing is done in pairs, level by level.

tx-atx-btx-ctx-d
8102aa5c6c28190cbcec62fcec18d2aa4866fc476d122482
f0683789eb93bd7128f6658d
cd31c5b2f295

Change the transaction list and watch the root change. The node recomputes this root when it validates every block — if one transaction was tampered with, the root won't match and the block is rejected.

4 · Consensus: who produces the block?

A blockchain needs a rule for deciding which block is the "real" next one, otherwise two nodes could disagree. Proof of Stake (PoS) says: the more coins you have locked up (staked), the more often you get to propose blocks — and the more you have to lose if you misbehave.

X-Change implements a deterministic, stake-weighted lottery:

apps/node/src/chain/consensus.rs
1// seed = sha256("proposer|{height}|{total_stake}")
2// pick = first 16 hex chars of seed as a number, mod total stake
3// walk validators by cumulative stake; the one you land on is the proposer
4pub fn select_proposer(validators: &[(String, i64)], height: i64) -> Option<String> { ... }

Because the seed depends only on height and total stake, every node computes the same proposer for a given height. No voting, no messages — just a hash.

Live: stake-weighted proposer selection

The node picks who produces the next block with a deterministic lottery: sha256("proposer|height|totalStake") turned into a number, walked through validators in proportion to their stake. The same height always picks the same proposer on every node.

0xaaa10%
0xbbb20%
0xccc70%PROPOSER
A demo, not a network
The real X-Change has one node. It runs the same proposer math, but with a single node it always proposes. In a real network many nodes would run this code and trust only blocks from the selected proposer.

5 · A signed transaction

A transaction moves value from one address to another. To prove the sender actually owns the from address, they sign a canonical message with their secret key. The node re-derives the message, checks the signature, and also checks the sender has enough balance and a matching nonce (a per-account counter that stops the same transaction being replayed).

Live: build and sign a real transaction

This is exactly what the frontend does before calling POST /tx/submit. The message is a canonical string; the hash is SHA-256 of everything including the signature.

from0x9f6b63a5ca…d00f
to
amountXCH
feeXCH (min 0.00001)
nonce7— the account's next transaction counter

Notice the hash includes the signature and the magic prefix XCH-1. When the node receives this, it recomputes the hash and verifies the signature against the public key — and that the public key derives to from.

Once validated, the transaction goes into the mempool (a waiting room). The next block produced includes it, and it's marked confirmed.

Recap — the five ideas

1. Hash — a fingerprint. Tampering is visible.
2. Keypair — secret signs, public verifies. Address = hash of pubkey.
3. Block + chain — each block links to the previous one's hash.
4. PoS — stake-weighted, deterministic proposer lottery.
5. Signed tx — prove ownership, check balance + nonce, confirm.

Now you're ready. Everything else is "where each of these five ideas lives in the code". Start with the Architecture Architecture Overview, then dive into the Backend Backend.