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
hash
function 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
Equals
override in C# has been generated by ReSharper (as wasGetHashCode
). Is the F#'sEquals
code good enough?
Blockchain.fs
- Does F# have a more suitable data structure for
chain
? - When invoking functions with a single
unit
parameter, is it good practice to write parenthesis or to omit them?
-
2\$\begingroup\$ It might be easier to review this if you just posted the f# code. Or maybe consider using quote blocks for the code that isn’t up for review. \$\endgroup\$RubberDuck– RubberDuck2018年09月23日 22:22:27 +00:00Commented Sep 23, 2018 at 22:22
-
\$\begingroup\$ @RubberDuck Here you are :) \$\endgroup\$Kapol– Kapol2018年09月23日 22:32:43 +00:00Commented Sep 23, 2018 at 22:32
1 Answer 1
Below find my comments and suggestions inline in the C# code (I'm not an expert on BlockChains so this is just use of my version of common sense and some programming experience):
public static class IntExtesions
{
public static string ToHex(this int i)
{
return i.ToString("x");
}
// For completeness make a ToHex for byte as well
public static string ToHex(this byte b)
{
return b.ToString("x2");
}
}
public class Block : IEquatable<Block>
{
public Block(int index, string previousHash, DateTime timestamp, string data, string hash, int difficulty, int nonce)
{
Index = index;
PreviousHash = previousHash;
Timestamp = timestamp;
Data = data;
Hash = hash;
Difficulty = difficulty;
Nonce = nonce;
}
// I'm not sure if an index is good as a property on the block.
//Shouldn't the index be determined by the position in the block chain?
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; } // I would keep the Nonce as a number
public int 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;
}
}
}
public class Blockchain
{
// Consider make Chain private and then implement IEnumerable<Block> on BlockChain.
// This will prevent "unauthorized" modification of the chain.
// You'll then have to create methods for Add and Remove, and Count etc.
public List<Block> Chain { get; private set; } = new List<Block>();
public int Difficulty { get; } = 1; // A difficulty of 1 seems not to be very "difficult" :-). Maybe it should be an argument to the constructor?
// It seems that you don't add the GenesisBlock to the Chain?
public Block GenesisBlock => new Block(0, "0", new
DateTime(2000, 1, 1), "Genesis block",
"816534932c2b7154836da6afc367695e6337db8a921823784c14378abed4f7d7",
1, 0);
// I'm not sure I understand what this method is good for?
public void ReplaceChain(List<Block> newChain)
{
if (newChain.Count > Chain.Count && ChainIsValid(newChain))
{
// Be aware that doing this:
// Chain = newChain;
// you actually only hold a reference to the incoming list. If someone outside this instance of BlockChain modifies that list, this instance may not behave as expected.
// Instead you should make a copy:
Chain = new List<Block>(newChain);
}
// TODO: If the new chain is rejected then the client should be nofitied: throw an exception or return false
}
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;
// With a modifed IsHasValid(...) (see below) you can do this:
while (!HashIsValid(nextHash, Difficulty))
{
nextHash = CalculateBlockHash(nextIndex, previousBlock.Hash, nextTimestamp, blockData, nonce);
nonce++; // Here you actually increment nonce once too much when the hash is valid
}
// ... instead of
//while (!hashIsValid)
//{
// hexNonce = nonce.ToHex();
// nextHash = CalculateBlockHash(nextIndex, previousBlock.Hash, nextTimestamp, blockData, hexNonce);
// if (HashIsValid(nextHash, Difficulty))
// {
// hashIsValid = true;
// }
// nonce++; // Here you actually increment nonce once too much when the hash is valid
//}
nonce--; // Decrement once for the last increment in the loop above
return new Block(nextIndex, previousBlock.Hash, nextTimestamp, blockData, nextHash, Difficulty, nonce);
}
// Instead of defining the hash computation once here, consider injecting an IHashCalculator { string ComputeHash(Block block); } into the BlockCain constructor
// It will make it possible for you to easily change the hash algorithm
public string CalculateBlockHash(int index, string previousHash, DateTime timestamp, string data, int nonce)
{
//var sb = new StringBuilder();
using (var hash = SHA256.Create())
{
// Instead of this...:
//var value = index +
// previousHash +
// timestamp.ToString(CultureInfo.InvariantCulture.DateTimeFormat.FullDateTimePattern) +
// data +
// nonce;
//var result = hash.ComputeHash(Encoding.UTF8.GetBytes(value));
// ... I would do something like this:
byte[] buffer =
BitConverter
.GetBytes(timestamp.Ticks)
.Concat(Encoding.UTF8.GetBytes(data))
.Concat(Encoding.Unicode.GetBytes(previousHash))
.Concat(BitConverter.GetBytes(nonce))
.ToArray();
var result = hash.ComputeHash(buffer);
// This can be done...
//foreach (var b in result)
// //sb.Append(b.ToString("x2")); // Make an extension for bytes too to keep it up to int
// sb.Append(b.ToHex());
// ... this way:
return string.Join("", result.Select(b => b.ToHex()));
}
//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)
{
// This can be done nicer
//var prefix = string.Concat(Enumerable.Repeat('0', difficulty));
var prefix = new string('0', difficulty);
// Check the hash for null allowing this method to be used more smoothly
return hash != null && hash.StartsWith(prefix);
}
private Block GetLatestBlock()
{
return Chain.Last();
}
}
Below find my review of the F# code:
open System
open System.Security.Cryptography
open System.Globalization
open System.Text
open System.Collections
open System.Collections.Generic
open System.Linq
// To create an extension function do this:
type Int32 with
member this.toHex() = this.ToString("x")
type Byte with
member this.toHex() = this.ToString("x2")
type Block(index: int, previousHash: string, timestamp: DateTime, data: string, blockHash: string, difficulty: int, nonce: int) =
member val Index = index // As for the C# version, Index is IMO redundant and potentially a source for inconsistency
member val PreviousHash = previousHash
member val Timestamp = timestamp
member val Data = data
member val Hash = blockHash
member val Difficulty = difficulty // I don't see the necessity for the Difficulty as a member on the block because it is invariant at the Block level?
member val Nonce = nonce
override this.Equals(obj) =
match obj with
| :? Block as b -> (index, previousHash, timestamp, data, blockHash) = (b.Index, b.PreviousHash, b.Timestamp, b.Data, b.Hash)
| _ -> false
// I think this:...
//override this.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
// ... can be simplified to this:
override this.GetHashCode() =
[
index;
(if previousHash <> null then previousHash.GetHashCode() else 0); // This should never be null.
timestamp.GetHashCode();
(if data <> null then data.GetHashCode() else 0); // I wonder if data will ever be null? Should it be possible to create a block without content?
(if blockHash <> null then blockHash.GetHashCode() else 0) // This should never be null.
]
|> List.fold (fun acc n -> (acc * 397) ^^^ n) 0
// If you will use hash in FSharp.Core.Operators it is more readable and produces the same hash
// So my guess is that it uses the same hash functions as you do - it's just a nice convenience function
//[
// hash index;
// hash previousHash;
// hash timestamp;
// hash data;
// hash blockHash
//]
//|> List.fold (fun acc n -> (acc * 397) ^^^ n) 0
// I think I would call it BlockCain because it's a concatenation of two words
type Blockchain() =
// You can use a Sytem.Collections.Generics.List instead of an array - it's more flexible but of cause more .NET-ish than functional
let mutable chain = new List<Block>([ Blockchain.GenesisBlock ])
// When implementing IEnumerable<Block> (see below) this should not be exposed as public
//member private this.Chain
// with get() = chain
// and set(value) = chain <- value
member val Difficulty = 1
// There is no reason to let this be part of the instance - it should be the same for all instances
static member GenesisBlock =
new Block(0, "0", new DateTime(2000, 1, 1), "Genesis block",
"816534932c2b7154836da6afc367695e6337db8a921823784c14378abed4f7d7", 1, 0) // The Difficulty argument is a "magic" number, which you may forget to synchronize with that of Blockchain - consider avoiding completely
member this.ReplaceChain(newChain: List<Block>) =
if newChain.Count > chain.Count && this.ChainIsValid newChain then
chain <- new List<Block>(newChain) // Make a copy instead of a reference
// I've made all the block generation functions private, because they should only be called once per new added block or at least called in a private context
member private this.GenerateNextBlock(blockData) =
let previousBlock: Block = this.GetLatestBlock()
let currentIndex = previousBlock.Index + 1 // currentIndex is better than nextIndex because you actually handle the current block
let currentTimestamp = DateTime.Now // current for the same reason as above.
let rec generateBlock nonce =
//let hexNonce = Int32.toHex(nonce) // Keep nonce as int
// currentHash is better than nextHash
let currentHash = this.CalculateBlockHash(currentIndex, previousBlock.Hash, currentTimestamp, blockData, nonce)
match this.HashIsValid(currentHash, this.Difficulty) with
| true -> new Block(currentIndex, previousBlock.Hash, currentTimestamp, blockData, currentHash, this.Difficulty, nonce - 1) // last recursion makes nonce one too large
| false -> generateBlock (nonce + 1)
generateBlock 0
// Again inject the hash calculation as an interface or function
member private this.CalculateBlockHash((index: int), previousHash, (timestamp: DateTime), data, nonce) =
use algorithm = SHA256.Create() // algorithm is a better name than hash
[index.ToString(); previousHash; timestamp.ToString(CultureInfo.InvariantCulture.DateTimeFormat.FullDateTimePattern); data; nonce.toHex()]
|> String.Concat
|> Encoding.UTF8.GetBytes
|> algorithm.ComputeHash
//|> Encoding.UTF8.GetString // I don't understand this?
//|> (+) "x2" // I don't understand this?
|> fun bytes -> String.Join("", bytes.Select(fun b -> b.toHex()))
member private this.CalculateBlockHash(block: Block) =
this.CalculateBlockHash(block.Index, block.PreviousHash, block.Timestamp, block.Data, block.Nonce)
member private this.ChainIsValid(chain) =
match chain.[0].Equals Blockchain.GenesisBlock with // Changed GenesisBlock from instance to static
| true -> chain |> Seq.pairwise |> Seq.forall this.BlockIsValid // The lambda wrapper is not necessary here: (fun (a, b) -> this.BlockIsValid(a, b))
| false -> false
member private this.BlockIsValid(previousBlock, newBlock) = // I've swapped previousBlock and newBlock, because that is the order in which the are passed to the function.
if previousBlock.Index + 1 <> newBlock.Index then
false
else if previousBlock.Hash <> newBlock.PreviousHash then
false
else
this.CalculateBlockHash newBlock = newBlock.Hash
member private this.HashIsValid(hash, difficulty) =
let prefix = String('0', difficulty) // (Seq.replicate difficulty '0') |> String.Concat
hash.StartsWith(prefix)
member private this.GetLatestBlock() = chain.Last()
// Add some new data, create a new Block and add it to the chain
member public this.AddData(data:string) =
if data = null then raise (ArgumentNullException("data"))
let block = this.GenerateNextBlock(data)
this.Add(block)
member public this.Add(block) = chain.Add(block)
// Implementation of IEnumerable<Block> instead of exposing the chain it self
interface IEnumerable<Block> with
member this.GetEnumerator() = (chain :> IEnumerable<Block>).GetEnumerator()
// Implementation of IEnumerable
interface IEnumerable with
member this.GetEnumerator() = (chain :> IEnumerable).GetEnumerator()
-
\$\begingroup\$ Thank you for the review. I skimmed through and your comments look very constructive. Please take these suggestions into account when reviewing the F# version. \$\endgroup\$Kapol– Kapol2018年09月24日 21:04:15 +00:00Commented Sep 24, 2018 at 21:04
-
1\$\begingroup\$
ReplaceChain
is needed because the blockchain is distributed, meaning that users hold copies of the blockchain and they may add new blocks to it. Different users may add different blocks at the same index leading to multiple "versions" of the blockchain existing at the same time. In this case the longest chain wins. \$\endgroup\$Kapol– Kapol2018年09月24日 21:18:38 +00:00Commented Sep 24, 2018 at 21:18 -
\$\begingroup\$ @Kapol: About
ReplaceChain
: Have you considered all concurrency situations properly? What if the incoming list is of same size but with the last block different due to an overlapping transaction? I can't figure out how that should be handled. Further: If you don't accept thenewChain
the client would probably be glad to know, or else the network of servers (blockchains) potentially can be inconsistent without no one to know. \$\endgroup\$user73941– user739412018年09月25日 04:49:58 +00:00Commented Sep 25, 2018 at 4:49 -
1\$\begingroup\$ I like these comments I don't understand this? ;-D could you also explain in more detail the changes you've made by giving a reason for them? Currently it's I've changed this to that but I'm missing the why like: it's shorter, faster, easier to test and whatnot ;-) \$\endgroup\$t3chb0t– t3chb0t2018年09月26日 07:22:35 +00:00Commented Sep 26, 2018 at 7:22
-
1