Some time ago I started writing a blockchain implementation for learning purposes. I used this article as reference. Originally I wrote the code in C# but recently I have rewritten everything to F#. Please review my F# code, although feel free to comment on C# as well if you feel like it.
Hopefully I will continue with my work and post more questions in the future. I plan to create further functionalities directly in F#.
Anyway, here is my C# code (so it's clear what exactly I wanted to achieve) and the F# equivalent. I have also added some questions at the end of the post.
Code
Helpers.cs
namespace Blockchain { public static class IntExtesions { public static string ToHex(this int i) { return i.ToString("x"); } } }
Helpers.fs
namespace Blockchain.Core
module Int32 =
let toHex(i: int) = i.ToString("x")
Block.cs
using System; namespace Blockchain.Core { public class Block : IEquatable<Block> { public Block(int index, string previousHash, DateTime timestamp, string data, string hash, int difficulty, string nonce) { Index = index; PreviousHash = previousHash; Timestamp = timestamp; Data = data; Hash = hash; Difficulty = difficulty; Nonce = nonce; } public int Index { get; } public string PreviousHash { get; } public DateTime Timestamp { get; } public string Data { get; } public string Hash { get; } public int Difficulty { get; } public string Nonce { get; } public bool Equals(Block other) { if (other is null) return false; if (ReferenceEquals(this, other)) return true; return Index == other.Index && string.Equals(PreviousHash, other.PreviousHash) && Timestamp.Equals(other.Timestamp) && string.Equals(Data, other.Data) && string.Equals(Hash, other.Hash); } public override bool Equals(object obj) { if (obj is null) return false; if (ReferenceEquals(this, obj)) return true; return obj.GetType() == this.GetType() && Equals((Block) obj); } public override int GetHashCode() { unchecked { var hashCode = Index; hashCode = (hashCode * 397) ^ (PreviousHash != null ? PreviousHash.GetHashCode() : 0); hashCode = (hashCode * 397) ^ Timestamp.GetHashCode(); hashCode = (hashCode * 397) ^ (Data != null ? Data.GetHashCode() : 0); hashCode = (hashCode * 397) ^ (Hash != null ? Hash.GetHashCode() : 0); return hashCode; } } } }
Block.fs
namespace Blockchain.Core
open System
type Block(index: int, previousHash: string, timestamp: DateTime, data: string, hash: string, difficulty: int, nonce: string) =
member val Index = index
member val PreviousHash = previousHash
member val Timestamp = timestamp
member val Data = data
member val Hash = hash
member val Difficulty = difficulty
member val Nonce = nonce
override x.Equals(obj) =
match obj with
| :? Block as b -> (index, previousHash, timestamp, data, hash) = (b.Index, b.PreviousHash, b.Timestamp, b.Data, b.Hash)
| _ -> false
override x.GetHashCode() =
let mutable hashCode = index
hashCode <- (hashCode * 397) ^^^ (if previousHash <> null then previousHash.GetHashCode() else 0)
hashCode <- (hashCode * 397) ^^^ timestamp.GetHashCode();
hashCode <- (hashCode * 397) ^^^ (if data <> null then data.GetHashCode() else 0)
hashCode <- (hashCode * 397) ^^^ (if hash <> null then hash.GetHashCode() else 0)
hashCode
Blockchain.cs
using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Security.Cryptography; using System.Text; namespace Blockchain.Core { public class Blockchain { public List<Block> Chain { get; private set; } = new List<Block>(); public int Difficulty { get; } = 1; public Block GenesisBlock => new Block(0, "0", new DateTime(2000, 1, 1), "Genesis block", "816534932c2b7154836da6afc367695e6337db8a921823784c14378abed4f7d7", 1, 0.ToHex()); public void ReplaceChain(List<Block> newChain) { if (newChain.Count > Chain.Count && ChainIsValid(newChain)) { Chain = newChain; } } public Block GenerateNextBlock(string blockData) { var previousBlock = GetLatestBlock(); var nextIndex = previousBlock.Index + 1; var nextTimestamp = DateTime.Now; var nonce = 0; bool hashIsValid = false; string hexNonce = null; string nextHash = null; while (!hashIsValid) { hexNonce = nonce.ToHex(); nextHash = CalculateBlockHash(nextIndex, previousBlock.Hash, nextTimestamp, blockData, hexNonce); if (HashIsValid(nextHash, Difficulty)) { hashIsValid = true; } nonce++; } return new Block(nextIndex, previousBlock.Hash, nextTimestamp, blockData, nextHash, Difficulty, hexNonce); } public string CalculateBlockHash(int index, string previousHash, DateTime timestamp, string data, string nonce) { var sb = new StringBuilder(); using (var hash = SHA256.Create()) { var value = index + previousHash + timestamp.ToString(CultureInfo.InvariantCulture.DateTimeFormat.FullDateTimePattern) + data + nonce; var result = hash.ComputeHash(Encoding.UTF8.GetBytes(value)); foreach (var b in result) sb.Append(b.ToString("x2")); } return sb.ToString(); } public string CalculateBlockHash(Block block) { return CalculateBlockHash(block.Index, block.PreviousHash, block.Timestamp, block.Data, block.Nonce); } private bool ChainIsValid(IReadOnlyList<Block> chain) { if (!chain[0].Equals(GenesisBlock)) { return false; } for (var i = 1; i < chain.Count; i++) { if (!BlockIsValid(chain[i], chain[i - 1])) { return false; } } return true; } private bool BlockIsValid(Block newBlock, Block previousBlock) { if (previousBlock.Index + 1 != newBlock.Index) { return false; } if (previousBlock.Hash != newBlock.PreviousHash) { return false; } return CalculateBlockHash(newBlock) == newBlock.Hash; } private static bool HashIsValid(string hash, int difficulty) { var prefix = string.Concat(Enumerable.Repeat('0', difficulty)); return hash.StartsWith(prefix); } private Block GetLatestBlock() { return Chain.Last(); } } }
Blockchain.fs
namespace Blockchain.Core
open System
open System.Security.Cryptography
open System.Globalization
open System.Text
type Blockchain() =
let mutable chain = [||] : Block array
member x.Chain
with get() = chain
and private set(value) = chain <- value
member val Difficulty = 1
member x.GenesisBlock =
new Block(0, "0", new DateTime(2000, 1, 1), "Genesis block",
"816534932c2b7154836da6afc367695e6337db8a921823784c14378abed4f7d7", 1, Int32.toHex(0))
member x.ReplaceChain(newChain: Block array) =
if newChain.Length > x.Chain.Length && x.ChainIsValid newChain then x.Chain <- newChain
member x.GenerateNextBlock(blockData) =
let previousBlock = x.GetLatestBlock()
let nextIndex = previousBlock.Index + 1
let nextTimestamp = DateTime.Now
let rec generateBlock nonce =
let hexNonce = Int32.toHex(nonce)
let nextHash = x.CalculateBlockHash(nextIndex, previousBlock.Hash, nextTimestamp, blockData, hexNonce)
match x.HashIsValid(nextHash, x.Difficulty) with
| true -> new Block(nextIndex, previousBlock.Hash, nextTimestamp, blockData, nextHash, x.Difficulty, hexNonce)
| false -> generateBlock(nonce + 1)
generateBlock 0
member x.CalculateBlockHash((index: int), previousHash, (timestamp: DateTime), data, nonce) =
use hash = SHA256.Create()
[index.ToString(); previousHash; timestamp.ToString(CultureInfo.InvariantCulture.DateTimeFormat.FullDateTimePattern); data; nonce]
|> String.Concat
|> Encoding.UTF8.GetBytes
|> hash.ComputeHash
|> Encoding.UTF8.GetString
|> (+) "x2"
member x.CalculateBlockHash(block: Block) =
x.CalculateBlockHash(block.Index, block.PreviousHash, block.Timestamp, block.Data, block.Nonce)
member private x.ChainIsValid(chain: Block array) =
match chain.[0].Equals x.GenesisBlock with
| true -> chain |> Seq.pairwise |> Seq.forall (fun (a, b) -> x.BlockIsValid(a, b))
| false -> false
member private x.BlockIsValid(newBlock: Block, previousBlock: Block) =
if previousBlock.Index + 1 <> newBlock.Index then
false
else if previousBlock.Hash <> newBlock.PreviousHash then
false
else
x.CalculateBlockHash newBlock = newBlock.Hash
member private x.HashIsValid((hash: string), difficulty) =
let prefix = (Seq.replicate difficulty '0') |> String.Concat
hash.StartsWith(prefix)
member private x.GetLatestBlock() = Array.last x.Chain
Questions
Block.fs
- There is a
hashfunction in F# but I couldn't find a definite answer if it can/should be used instead ofGetHashCode. Are there some useful good practices? - The
Equalsoverride in C# has been generated by ReSharper (as wasGetHashCode). Is the F#'sEqualscode good enough?
Blockchain.fs
- Does F# have a more suitable data structure for
chain? - When invoking functions with a single
unitparameter, is it good practice to write parenthesis or to omit them?