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).
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.
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:
1// address = "0x" + hex(sha3-256(pubkey)[..20])2export function deriveAddress(rawPubkey: Uint8Array): string {3return "0x" + bytesToHex(sha3(rawPubkey).subarray(0, 20));4}
This is the same ed25519 signing the app uses. The secret key signs; anyone with the public key can verify — but cannot forge.
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.
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:
1pub struct Block {2pub index: i64, // block number, starts at 0 (genesis)3pub hash: String, // sha256 of the fields below4pub prev_hash: String, // hash of the previous block <-- THE LINK5pub merkle_root: String, // one hash summarising all tx hashes6pub proposer: String, // who produced this block7pub tx_hashes: Vec<String>,8pub created_at: i64, // unix milliseconds9}
A new block is produced every 3 seconds on a timer. Its hash is:
1fn compute_hash(&self) -> String {2let raw = format!(3"{}|{}|{}|{}|{}|{}",4self.index, self.prev_hash, self.merkle_root,5self.proposer, self.tx_hashes.join(","), self.created_at6);7sha256hex(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.
In a block, the list of transactions is summarised into a single 64-char Merkle root. Hashing is done in pairs, level by level.
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:
1// seed = sha256("proposer|{height}|{total_stake}")2// pick = first 16 hex chars of seed as a number, mod total stake3// walk validators by cumulative stake; the one you land on is the proposer4pub 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.
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%PROPOSER5 · 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).
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.
0x9f6b63a5ca…d00f7— the account's next transaction counterNotice 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
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.