Tutorials → 01

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:

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:

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.

Sources/MyFirstChain/main.swift
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
)
Chain Spec parameters

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)
What happens at init
The node constructor runs the genesis ceremony — it creates the first block (block 0), initializes the chain state, and sets up the storage layer. No network connections are made yet.

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")")
Transaction anatomy

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

LayerWhat it does
LatticeNodeOrchestrates everything: storage, mining, networking, mempool
ChainStateTracks all blocks, the main chain, fork choice via cumulative work
MinerFinds nonces that satisfy the difficulty target (proof-of-work)
MempoolHolds pending transactions until a miner includes them in a block
Acorn CASContent-addressed storage for blocks, state trees, and transaction data