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 aBlock
)
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
comments powered by Disqus