Switch branches/tags
Nothing to show
Find file History
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
..
Failed to load latest commit information.
src
test
.gitignore
Makefile
README.md
package.yaml
stack.yaml

README.md

the chain in blockchain explained

This post develops a simple blockchain with the goal of understanding the basics of the chain.

setup:

{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}

module Chain where

import           ClassyPrelude         as CP
import           Crypto.Hash.SHA256    as C  (hash)
import           Data.ByteString       as BS (concat)
import           Data.ByteString.Char8 as BS (pack)
import qualified Prelude               as P  (tail)
import           Data.Sequence         as S
import           Test.Hspec

The most straigtforward part of a blockchain is the chain itself, a sequence of blocks:

type Blockchain = Seq Block

For the purposes of this exposition, a block in the chain is:

data Block =
  Block { bIndex     :: ! BIndex     -- ^ index of this block in the chain -- for debugging
        , bPrevHash  :: ! BHash      -- ^ hash of previous block
        , bTimestamp :: ! BTimestamp -- ^ when this block was created
        , bData      :: ! BData      -- ^ this block's data
        } deriving (Eq, Show)

where

type BIndex     = Int
type BHash      = ByteString
type BTimestamp = ByteString
type BData      = ByteString

A hard-coded genesis block:

genesisBlock :: Block
genesisBlock =
 let idx      = 0
     prevHash = "0"
     ts       = "2017-03-05 10:49:02.084473 PST"
     bdata    = "GENESIS BLOCK DATA"
 in Block idx prevHash ts bdata

begins the chain:

genesisBlockchain :: Blockchain
genesisBlockchain  = S.singleton genesisBlock

For this exposition, the only purpose of the genesis block is to provide a verifiable prevHash for the first "real" block that gets added to the chain.

The chain is tamper-proof because each block contains a hash of of the contents of the previous block:

calculateHash :: BIndex -> BHash -> BTimestamp -> BData -> BHash
calculateHash i p t d = C.hash (BS.concat [BS.pack $ show i, p, t, d])

Since a block contains the hash of the previous block, once a block has been added to a chain, the previous contents of the chain cannot be altered without detection.

For example, using the following functions:

addBlock :: BTimestamp -> BData -> Blockchain -> Blockchain
addBlock ts bd bc = bc |> makeNextBlock bc ts bd

makeNextBlock :: Blockchain -> BTimestamp -> BData -> Block
makeNextBlock bc ts bd =
  let (i, ph, _, _) = nextBlockInfo bc ts bd
  in Block i ph ts bd

nextBlockInfo :: Blockchain -> BTimestamp -> BData
              -> (BIndex, BHash, BTimestamp, BData)
nextBlockInfo bc ts bd =
  let prev = getLastCommittedBlock bc
      i    = bIndex prev + 1
      ph   = calculateHash (bIndex prev) (bPrevHash prev) (bTimestamp prev) (bData prev)
  in (i, ph, ts, bd)

getLastCommittedBlock :: Blockchain -> Block
getLastCommittedBlock bc = S.index bc (S.length bc - 1)

a new block may be added to the chain:

t1 :: Spec
t1 =
  let newChain = addBlock "2017-06-11 15:49:02.084473 PST"
                          "June 11 data"
                          genesisBlockchain
  in describe "t1: add new block to chain" $ do
       it "increases length" $
         S.length newChain `shouldBe` S.length genesisBlockchain + 1
       it "does not change existing blocks" $
         getBlock newChain 0 `shouldBe` Just genesisBlock

where

-- | Nothing if index out of range.
getBlock :: Blockchain -> BIndex -> Maybe Block
getBlock bc i = S.lookup i bc

The chained block hashes ensure the integrity of a blockchain. Modification of a parts of any blocks in the chain is detected:

t2 :: Spec
t2 =
  let newChain = addBlock "2017-06-12 15:49:02.084473 PST"
                          "June 12 data"
                          (addBlock "2017-06-11 15:49:02.084473 PST"
                                    "June 11 data"
                                    genesisBlockchain)
      altered1i  = (S.index newChain 1) { bIndex = 10 }
      badChain1i = S.update 1 altered1i newChain
      altered1p  = (S.index newChain 1) { bPrevHash = "0" }
      badChain1p = S.update 1 altered1p newChain
      altered1d  = (S.index newChain 1) { bData  = "altered June 11 data" }
      badChain1d = S.update 1 altered1d newChain
      altered12  = (S.index newChain 2) { bData  = "altered June 12 data" }
      badChain12 = S.update 2 altered12 newChain
  in describe "t2: valid blockchain" $ do
    it "invalid empty" $
      isValidBlockchain S.empty    `shouldBe` Left "empty blockchain"
    it "valid genesisblockchain" $
      isValidBlockchain genesisBlockchain
                                   `shouldBe` Right ()
    it "invalid genesisblock" $
      isValidBlockchain (S.singleton altered12)
                                   `shouldBe` Left "invalid genesis block"
    it "valid newChain" $
      isValidBlockchain newChain   `shouldBe` Right ()
    it "invalid bIndex 1" $
      isValidBlockchain badChain1i `shouldBe` Left "invalid bIndex 1"
    it "invalid bPrevHash 1" $
      isValidBlockchain badChain1p `shouldBe` Left "invalid bPrevHash 1"
    it "invalid bPrevHash 2" $
      isValidBlockchain badChain1d `shouldBe` Left "invalid bPrevHash 2"
    -- This shows that it IS possible to alter the last block in the chain.
    -- In a "real" blockchain, consensus and subsequent blocks added to
    -- the chain handles this case.
    it "changed last element in chain" $
      isValidBlockchain badChain12 `shouldBe` Right ()

where

-- | Returns `Just ()` if valid.
-- otherwise `Left reason`
isValidBlockchain :: Blockchain -> Either Text ()
isValidBlockchain bc = do
  when (S.length bc == 0)                                 (Left "empty blockchain")
  when (S.length bc == 1 && S.index bc 0 /= genesisBlock) (Left "invalid genesis block")
  let elements = toList bc
  -- `sequence_` causes function to return on/with first `Left` value
  sequence_ (CP.map isValidBlock (CP.zip elements (P.tail elements)))
  return ()

-- | Given a valid previous block and a block to check.
-- | Returns `Just ()` if valid.
-- otherwise `Left reason`
isValidBlock :: (Block, Block) -> Either Text ()
isValidBlock (validBlock, checkBlock) = do
  when (bIndex    validBlock + 1 /= bIndex    checkBlock) (fail' "invalid bIndex")
  when (hashBlock validBlock     /= bPrevHash checkBlock) (fail' "invalid bPrevHash")
  return ()
 where
  fail' msg   = Left (msg <> " " <> tshow (bIndex validBlock + 1))
  hashBlock b = calculateHash (bIndex b) (bPrevHash b) (bTimestamp b) (bData b)

The above is the essence of the chain in blockchain.

future expositions

Note that there was no discussion of

  • how to decide to add a block to a chain (e.g., mining, consensus)
  • merkle trees (i.e., an efficient, tamper-proof way to structure bData to hold more than one "transaction")
  • smart contracts (i.e., how to interpret the bData field in a Block)

summary

A blockchain is a list of blocks, where each block

  • contains data that may be interpreted as smart contracts
  • contains the hash of the previous block
  • the chained hashes makes the chain tamper-proof
t3 :: Spec
t3 =
  let withOne = addBlock "2017-06-11 15:49:02.084473 PST"
                         "June 11 data"
                         genesisBlockchain
      withTwo = addBlock "2017-06-12 15:49:02.084473 PST"
                         "June 12 data"
                         withOne
  in describe "t3: list" $
     it "withTwo" $
     withTwo `shouldBe`
     S.fromList
     [ Block { bIndex = 0
             , bPrevHash = "0"
             , bTimestamp = "2017-03-05 10:49:02.084473 PST"
             , bData = "GENESIS BLOCK DATA"
             }
     , Block { bIndex = 1
             , bPrevHash = "'\234-\147\141\"\142\235\CAN \246\158<\159\199s\174\\\225<\174\188O\150oM\217\DC3'\237\DC4n"
             , bTimestamp = "2017-06-11 15:49:02.084473 PST"
             , bData = "June 11 data"
             }
     , Block { bIndex = 2
             , bPrevHash = "\145\238k24\175\147I\EOT\208\204\210\190s\192<b:\SOH\215\DC1\254)\173\EOT\186\220\US\SYNf\191\149"
             , bTimestamp = "2017-06-12 15:49:02.084473 PST"
             , bData = "June 12 data"
             }
     ]

source code and discussion

Thanks to Ulises Cerviño Beresi, Heath Matlock, Alejandro Serra Mena and Mark Moir for pre-publication feedback.

The code for this exposition is available at : github

run the code: stack test

A discussion is at: reddit