Consensus Algorithms: Why Nodes Trust Each Other

In a decentralized system, there’s no central server telling nodes what’s true. So how do nodes agree on the same version of the blockchain?

That’s where consensus algorithms come in. They ensure that every participant in a blockchain network agrees on a single, valid history — even if some nodes go offline or act maliciously.

In this article, we’ll break down how consensus works, compare common algorithms, and simulate a simple consensus mechanism in Go to see it in action.

Why Do We Need Consensus?

In a blockchain:

  • Anyone can propose a new block.
  • Only valid blocks should be added.
  • Every node should agree on the same chain.

Without consensus, you'd get:

  • Forks everywhere
  • Conflicting transactions
  • No trust in the system

Consensus = coordination in a trustless world.

Popular Consensus Algorithms (At a Glance)
Algorithm Used By Key Idea
Proof of Work Bitcoin Nodes compete to solve a puzzle
Proof of Stake Ethereum (post-Merge) Nodes are chosen based on stake (coins held)
PBFT Hyperledger, Cosmos Nodes vote and reach majority agreement
Proof of Authority Private chains Identity-based trusted validators

We’ll focus on simulating the core idea that underpins most of these: Agreement on the longest, valid chain.

Simulating Simple Consensus in Go

Let’s simulate a network of nodes. Each node:

  • Stores a copy of the blockchain
  • Receives new blocks from peers
  • Accepts the longest valid chain as truth

This is the basic consensus logic behind Proof of Work blockchains like Bitcoin. See next section for more.

Go example

package main

import (
  "crypto/sha256"
  "encoding/hex"
  "fmt"
  "strconv"
  "strings"
  "time"
)

// Block represents a block in the blockchain
type Block struct {
  Index     int
  Timestamp string
  Data      string
  PrevHash  string
  Hash      string
  Nonce     int
}

// Node represents a node in the network
type Node struct {
  ID         string
  Blockchain []Block
}

// Calculates the SHA-256 hash of a block including its nonce
func calculateHash(block Block) string {
  record := strconv.Itoa(block.Index) + block.Timestamp + block.Data + block.PrevHash + strconv.Itoa(block.Nonce)
  hash := sha256.Sum256([]byte(record))
  return hex.EncodeToString(hash[:])
}

// Proof of Work mining function
func mineBlock(block *Block, difficulty int) {
  targetPrefix := strings.Repeat("0", difficulty)
  for {
    hash := calculateHash(*block)
    if strings.HasPrefix(hash, targetPrefix) {
      block.Hash = hash
      return
    }
    block.Nonce++
  }
}

// Creates the first block (genesis block)
func generateGenesisBlock(difficulty int) Block {
  block := Block{
    Index:     0,
    Timestamp: time.Now().String(),
    Data:      "Genesis Block",
    PrevHash:  "",
  }
  mineBlock(█, difficulty)
  return block
}

// Mines and returns the next block based on the previous one
func mineNewBlock(prevBlock Block, data string, difficulty int) Block {
  newBlock := Block{
    Index:     prevBlock.Index + 1,
    Timestamp: time.Now().String(),
    Data:      data,
    PrevHash:  prevBlock.Hash,
  }
  mineBlock(&newBlock, difficulty)
  return newBlock
}

// Validates a full blockchain (links and hashes must match)
func isValidChain(chain []Block) bool {
  for i := 1; i < len(chain); i++ {
    prev := chain[i-1]
    curr := chain[i]
    if curr.PrevHash != prev.Hash {
      return false
    }
    if calculateHash(curr) != curr.Hash {
      return false
    }
  }
  return true
}

// Simulates consensus: adopt the longest valid chain from neighbors
func resolveConflict(node *Node, neighbors []*Node) {
  maxLength := len(node.Blockchain)
  var longestChain []Block

  for _, peer := range neighbors {
    if isValidChain(peer.Blockchain) && len(peer.Blockchain) > maxLength {
      maxLength = len(peer.Blockchain)
      longestChain = peer.Blockchain
    }
  }

  if longestChain != nil {
    node.Blockchain = longestChain
    fmt.Printf("Node %s adopted a longer chain from peers.\n", node.ID)
  }
}

// Display the chain for debugging or verification
func printChain(node *Node) {
  fmt.Printf("\nBlockchain at node %s:\n", node.ID)
  for _, block := range node.Blockchain {
    fmt.Printf("Index: %d, Nonce: %d, Hash: %s\n", block.Index, block.Nonce, block.Hash)
  }
}

func main() {
  difficulty := 3

  // Genesis block shared across all nodes
  genesis := generateGenesisBlock(difficulty)

  // Simulate three nodes in a network
  nodeA := &Node{ID: "A", Blockchain: []Block{genesis}}
  nodeB := &Node{ID: "B", Blockchain: []Block{genesis}}
  nodeC := &Node{ID: "C", Blockchain: []Block{genesis}}

  // Node A mines 2 blocks
  nodeA.Blockchain = append(nodeA.Blockchain, mineNewBlock(nodeA.Blockchain[len(nodeA.Blockchain)-1], "Tx A1", difficulty))
  nodeA.Blockchain = append(nodeA.Blockchain, mineNewBlock(nodeA.Blockchain[len(nodeA.Blockchain)-1], "Tx A2", difficulty))

  // Node B mines 1 block
  nodeB.Blockchain = append(nodeB.Blockchain, mineNewBlock(nodeB.Blockchain[len(nodeB.Blockchain)-1], "Tx B1", difficulty))

  // Node C receives chains from A and B, and adopts the longest valid one
  neighbors := []*Node{nodeA, nodeB}
  resolveConflict(nodeC, neighbors)

  // Print results
  printChain(nodeA)
  printChain(nodeB)
  printChain(nodeC)
}
What We Simulated

Node A had the longest chain.

Node C accepted Node A’s chain over Node B’s.

Consensus was reached: everyone trusts the longest valid chain.

This is a simplified version of how Bitcoin nodes operate: trust the chain with the most work behind it.

Consensus in the Real World

Benefits:

  • Tolerance to node failure or delay
  • No central authority required
  • Secure against malicious actors (up to a point)

Limitations:

  • Proof of Work is energy-intensive
  • Finality (when a block is truly irreversible) can be slow
  • Forks and chain reorganizations can occur