Your First Chain
Create a Lattice node, start mining, submit a transaction, and query chain state. Under 50 lines of Swift.
What You'll Build
A standalone blockchain node that:
- Creates a genesis block with a custom chain specification
- Mines blocks with proof-of-work
- Submits a state-mutating transaction
- Queries chain height and block data
Step 1: Create the Project
The fastest way is with the CLI:
lattice init my-first-chain
cd my-first-chain
Or create it manually. Your Package.swift:
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "MyFirstChain",
platforms: [.macOS(.v15)],
dependencies: [
.package(url: "https://github.com/treehauslabs/lattice.git", branch: "main"),
.package(url: "https://github.com/treehauslabs/AcornMemoryWorker.git", branch: "main"),
],
targets: [
.executableTarget(name: "MyFirstChain", dependencies: [
.product(name: "Lattice", package: "lattice"),
.product(name: "AcornMemoryWorker", package: "AcornMemoryWorker"),
]),
]
)
Step 2: Define the Chain Spec
A ChainSpec defines the rules of your blockchain — block timing, rewards, capacity limits. Think of it as the constitution your chain will operate under.
import Foundation
import Lattice
import AcornMemoryWorker
let spec = ChainSpec(
directory: "MyChain", // unique chain identifier
maxNumberOfTransactionsPerBlock: 50, // transactions per block
maxStateGrowth: 50_000, // max state bytes per block
premine: 0, // no pre-mined blocks
targetBlockTime: 2_000, // 2 second blocks
initialRewardExponent: 10 // reward = 2^10 = 1024 per block
)
directory is the chain's name — like a namespace. The root chain is typically "Nexus".
targetBlockTime is in milliseconds. Difficulty auto-adjusts to maintain this pace.
initialRewardExponent sets the mining reward as 2n. Rewards halve periodically like Bitcoin.
Step 3: Generate Keys and Create the Node
Every node needs an identity — a P256 keypair. The public key is your node's address on the network.
let keyPair = CryptoUtils.generateKeyPair()
let address = CryptoUtils.createAddress(from: keyPair.publicKey)
print("Node address: \(address)")
let genesisConfig = GenesisConfig.standard(spec: spec)
let nodeConfig = LatticeNodeConfig(
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey,
listenPort: 4001,
storagePath: URL(filePath: "/tmp/my-first-chain"),
enableLocalDiscovery: true
)
let node = try await LatticeNode(config: nodeConfig, genesisConfig: genesisConfig)
Step 4: Start the Node and Mine
try await node.start()
print("Node started on port \(nodeConfig.listenPort)")
// Start mining on our chain
await node.startMining(directory: "MyChain")
print("Mining started...")
start() brings up the P2P networking layer and begins accepting connections. startMining spins up the proof-of-work miner for the specified chain directory.
Step 5: Query Chain State
Give the miner a few seconds to produce blocks, then query the chain:
// Wait for a few blocks to be mined
try await Task.sleep(for: .seconds(10))
let chainState = await node.genesisResult.chainState
let height = await chainState.getHighestBlockIndex()
let tip = await chainState.getMainChainTip()
print("Chain height: \(height)")
print("Chain tip: \(tip)")
// Get the latest block details
let latestBlock = await chainState.getHighestBlock()
print("Latest block hash: \(latestBlock.blockHash)")
print("Previous block: \(latestBlock.previousBlockHash ?? "genesis")")
Step 6: Submit a Transaction
Transactions mutate on-chain state. Each transaction contains actions — key-value operations — and must be signed by its sender.
// Create a state-mutating action: set key "greeting" to "hello lattice"
let action = Action(
key: "greeting",
oldValue: nil, // nil = this key doesn't exist yet (insertion)
newValue: "hello lattice" // the value to store
)
// Build the transaction body
let body = TransactionBody(
accountActions: [],
actions: [action],
depositActions: [],
genesisActions: [],
peerActions: [],
receiptActions: [],
withdrawalActions: [],
signers: [CryptoUtils.sha256(keyPair.publicKey)],
fee: 1,
nonce: 0
)
// Sign and submit
let bodyData = body.toData()
let bodyHash = CryptoUtils.sha256(String(data: bodyData, encoding: .utf8)!)
let signature = CryptoUtils.sign(message: bodyHash, privateKeyHex: keyPair.privateKey)!
let transaction = Transaction(
signatures: [keyPair.publicKey: signature],
body: body
)
let accepted = await node.submitTransaction(directory: "MyChain", transaction: transaction)
print("Transaction \(accepted ? "accepted" : "rejected")")
Actions are state mutations: key + oldValue + newValue. Setting oldValue: nil creates a new key; setting newValue: nil deletes one.
Signers is the SHA256 hash of each public key that must sign. The signatures map proves authorization.
Fee goes to the miner who includes your transaction in a block.
Step 7: Graceful Shutdown
await node.stopMining(directory: "MyChain")
await node.stop()
print("Node stopped")
Run It
swift run
You should see:
Node address: 0x3a7f...
Node started on port 4001
Mining started...
Chain height: 4
Chain tip: a8c3e9f1...
Transaction accepted
Node stopped
What's Happening Under the Hood
| Layer | What it does |
|---|---|
| LatticeNode | Orchestrates everything: storage, mining, networking, mempool |
| ChainState | Tracks all blocks, the main chain, fork choice via cumulative work |
| Miner | Finds nonces that satisfy the difficulty target (proof-of-work) |
| Mempool | Holds pending transactions until a miner includes them in a block |
| Acorn CAS | Content-addressed storage for blocks, state trees, and transaction data |