-
-
Save lontivero/2e5ab7866399df1991ec6dc274175b1e to your computer and use it in GitHub Desktop.
Using NBitcoin to create private BlockChain with F# (FSharp)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is just an initial example / tech-demo. | |
#if INTERACTIVE | |
#I "./../packages/NBitcoin/lib/net45/" | |
#I "./../packages/Newtonsoft.Json/lib/net45" | |
#r "NBitcoin.dll" | |
#r "Newtonsoft.Json.dll" | |
#else | |
module BlockChain | |
#endif | |
open System | |
open NBitcoin | |
open NBitcoin.Protocol | |
// -------------- GENERAL BLOCKCHAIN NETWORK SETTINGS -------------- // | |
let network = | |
// Select your network | |
// Network.Main | |
// Network.Test | |
// Or create your own: | |
let builder = NetworkBuilder() | |
builder.CopyFrom Network.Main | |
let genesis = Network.Main.GetGenesis() | |
//builder.SetGenesis( {genesis.Header.UpdateTime with Ti } | |
builder.SetName("MyBlockChain").BuildAndRegister() | |
/// Generate a new wallet private key for a new user as byte array | |
let getNewPrivateKey() = Key().ToBytes() | |
/// Create BitcoinSecret from byte array | |
let getSecret(bytes:byte[]) = BitcoinSecret(Key(bytes), network) | |
// --------- CUSTOM BLOCKCHAIN AND ITS COMPLEXITY --------- // | |
/// Complexity of valid mining. Genesis is having 10. | |
/// But this comes down to how much resources you are willing to | |
/// spend to validate blockchain. Note: Some part of blockchain | |
/// security comes from the fact that valid hashes are not so easy | |
/// to calculate, making the generation of alternative valid blockchain slow. | |
let leadingZeros = "0000" | |
/// Validation of blockchain hashes. | |
type BlockChainCheck = | |
/// No validation | |
| NoWork | |
/// Validation of leadingZeros amount only | |
| EasyWork | |
/// Default Bitcoin level validation | |
| CorrectWork | |
/// The normal bitcoin validation is leading zeros in hashcodes. | |
/// Normal mining taking a lot of resources. So this is more | |
/// lightweight option to mining (with less zeros). | |
type ChainedBlock with | |
/// Re-implementation of Validate() | |
member cb.ValidateEasy(network:Network) = | |
let genesis = cb.Height = 0 | |
if (not genesis) && cb.Previous = null then false | |
else | |
let heightCorrect = genesis || cb.Height = cb.Previous.Height + 1 | |
let genesisCorrect = | |
(not genesis) || cb.HashBlock = network.GetGenesis().GetHash() | |
let hashPrevCorrect = | |
genesis || cb.Header.HashPrevBlock = cb.Previous.HashBlock | |
let hashCorrect = cb.HashBlock = cb.Header.GetHash() | |
let workCorrect = | |
genesis || cb.Header.GetHash().ToString().StartsWith leadingZeros | |
heightCorrect && genesisCorrect && hashPrevCorrect | |
&& hashCorrect && workCorrect | |
type ChainBase with | |
/// Re-implementation of Validate() | |
member cb.ValidateEasy(network:Network, ?fullChain) = | |
let tip = cb.Tip | |
if tip = null then false | |
else | |
match fullChain with | |
| None | Some true -> | |
let anyFails = | |
tip.EnumerateToGenesis() | |
|> Seq.exists(fun block -> not(block.ValidateEasy network)) | |
not anyFails | |
| Some false -> | |
tip.ValidateEasy network | |
/// This will mine the correct hash. | |
/// Performance is not optimized: if you would really want to do | |
/// mining, you would probably want to use some more parallel algorithm. | |
let ``mine to correct`` checkType (chain:ConcurrentChain) (block:Block) = | |
let validation = | |
match checkType with | |
| NoWork -> fun (cb:ChainedBlock) -> true | |
| EasyWork -> fun cb -> cb.ValidateEasy network | |
| CorrectWork -> fun cb -> cb.Validate network | |
let rec mine nonce = | |
block.Header.Nonce <- nonce | |
let headerBlock = | |
ChainedBlock(block.Header, block.Header.GetHash(), | |
chain.GetBlock(block.Header.HashPrevBlock)) | |
if validation headerBlock then () | |
else mine (nonce+1u) | |
mine 0u | |
/// Attach a block to chain | |
let ``attach to chain`` (checkType:BlockChainCheck) (chain:ConcurrentChain) (block:Block) = | |
let header = block.Header | |
header.HashPrevBlock <- chain.Tip.HashBlock | |
header.Bits <- | |
//header.GetWorkRequired(network, chain.Tip) | |
chain.Tip.GetWorkRequired(network) | |
header.BlockTime <- DateTimeOffset.UtcNow | |
header.Nonce <- RandomUtils.GetUInt32() | |
//header.UpdateTime(network, chain.Tip) | |
block.UpdateMerkleRoot() | |
``mine to correct`` checkType chain block | |
chain.SetTip header |> ignore | |
chain.GetBlock(header.GetHash()) | |
/// Attach a bunch of transactions to a new block | |
let ``to new block`` txs = | |
let block = Block() | |
txs |> Seq.iter (block.AddTransaction >> ignore) | |
block.UpdateMerkleRoot() | |
block | |
// --------- TRANSACTIONS --------- // | |
/// No fees on our custom block-chain | |
/// Consifer adding some fee when you want users to do the mining. | |
let noFees = Unchecked.defaultof<FeeRate> | |
let txBuilder() = | |
let b = TransactionBuilder() | |
b.StandardTransactionPolicy.MinRelayTxFee <- FeeRate.Zero | |
b | |
/// Coinbase transaction is a transaction to generate money to our system. | |
let ``give money`` (toUser:BitcoinSecret) (sum:int) = | |
let money = Money sum | |
let coin = | |
Coin( // Coins for sums, ColoredCoins for assets | |
// This is a coinbase / generation transaction, so prev hash is zero: | |
OutPoint(), TxOut(money, toUser.PubKey.ScriptPubKey) | |
) | |
let builder = txBuilder() | |
let tx = | |
builder | |
//.IssueAsset(toUser, OpenAsset.AssetMoney(asset.AssetId, quantity)) | |
.AddCoins([| (coin :> ICoin) |]) | |
.AddKeys(toUser) | |
.Send(toUser, money) | |
.SetChange(toUser.GetAddress()) | |
.BuildTransaction(true) | |
if not tx.IsCoinBase then | |
failwith "Was not a coinbase transaction" | |
let ok, errs = builder.Verify(tx, noFees) | |
match ok with | |
| true -> tx | |
| false -> failwith(String.Join(",", errs)) | |
/// Normal transaction to transfer money / asset from user to next user. | |
/// Needs outpoint (coins) from previous transaction to make a block chain. | |
let ``spend money`` (coins:Coin list) (fromUser:BitcoinSecret) (toUser:BitcoinPubKeyAddress) (sum:int) = | |
let money = Money sum | |
let fees = Money.Zero | |
let coinsArr = | |
coins | |
|> List.filter(fun c -> c.TxOut.IsTo fromUser) | |
|> List.map(fun c -> c :> ICoin) |> List.toArray | |
let builder = txBuilder() | |
let tx = | |
builder | |
.AddCoins(coinsArr) | |
.AddKeys(fromUser) | |
.Send(toUser, (money - fees)) | |
.SendFees(fees) | |
.SetChange(fromUser.GetAddress()) | |
.BuildTransaction(true) | |
let ok, errs = builder.Verify(tx, noFees) | |
match ok with | |
| true -> tx | |
| false -> failwith(String.Join(",", errs)) | |
// --------- SOME HELPER FUNCTIONS --------- // | |
/// Create a new user | |
let makeUser() = | |
let user = BitcoinSecret(Key(), network) | |
Console.WriteLine "Store users private key to a cold dry place and never show it to anyone:" | |
network |> user.PrivateKey.GetWif |> Console.WriteLine | |
user | |
/// Get users coins from transaction | |
let ``fetch coins of`` dest (tx:Transaction) = | |
tx.Outputs | |
|> Seq.mapi(fun idx op -> idx,op) | |
|> Seq.filter(fun (_,op) -> op.Value > Money.Zero && op.IsTo(dest)) | |
|> Seq.map(fun (idx,op) -> | |
Coin(OutPoint(tx, idx), op) | |
) |> Seq.toList | |
let ``save tracker`` filename (tracker:NBitcoin.SPV.Tracker) = | |
let filterBefore = tracker.CreateBloomFilter(0.005) | |
use fileStream = System.IO.File.Create filename | |
tracker.Save fileStream | |
let ``load tracker`` filename = | |
let tracker = | |
filename |> System.IO.File.OpenRead | |
|> NBitcoin.SPV.Tracker.Load | |
if not(tracker.Validate()) then failwith "Not valid tracker" | |
else tracker | |
let ``save chain`` filename (chain:ConcurrentChain) = | |
use fileStream = System.IO.File.Create filename | |
chain.WriteTo (BitcoinStream(fileStream, true)) | |
let ``load chain`` filename = | |
let chain = new ConcurrentChain(network) | |
filename |> System.IO.File.ReadAllBytes |> chain.Load | |
// if not(chain.Validate network) then failwith "Not valid" | |
if not(chain.ValidateEasy network) then failwith "Not valid chain" | |
else chain | |
// --------- TESTING --------- // | |
let testing() = | |
// A block chain, and a tracker | |
let chain = new ConcurrentChain(network) | |
let tracker = NBitcoin.SPV.Tracker() | |
// make some users | |
let thomas = makeUser() | |
let antero = makeUser() | |
let john = makeUser() | |
tracker.Add thomas | |
tracker.Add antero | |
tracker.Add john | |
// Make Block 1 with transactions and add it to chain | |
let coinbase1 = ``give money`` thomas 1000 | |
let coins1 = coinbase1 |> ``fetch coins of`` thomas | |
let transfer1 = | |
``spend money`` coins1 thomas (antero.GetAddress()) 500 | |
let coins2 = transfer1 |> ``fetch coins of`` thomas | |
let transfer2 = ``spend money`` coins2 thomas (john.GetAddress()) 300 | |
let block1 = | |
[coinbase1; transfer1; transfer2] | |
|> ``to new block`` | |
let chained1 = | |
block1 |> ``attach to chain`` BlockChainCheck.EasyWork chain | |
block1.Transactions |> Seq.iter (fun tx -> | |
tracker.NotifyTransaction(tx, chained1, block1) |> ignore) | |
// Check that chain and the tracker are valid: | |
let ``chain ok`` = chain.ValidateEasy network | |
let ``tracker ok`` = tracker.Validate() | |
let transactions1 = tracker.GetWalletTransactions chain | |
//transactions1.GetSpendableCoins() | |
//transactions1.Summary.Confirmed | |
// Thomas: 200, Antero: 500, John: 300 | |
// Make Block 2 with transactions and add it to chain | |
let coinbase2 = ``give money`` thomas 100 | |
let coins3 = transfer1 |> ``fetch coins of`` antero | |
let transfer3 = ``spend money`` coins3 antero (john.GetAddress()) 500 | |
let coins4 = | |
(transfer2 |> ``fetch coins of`` thomas) @ | |
(coinbase2 |> ``fetch coins of`` thomas) | |
let transfer4 = | |
``spend money`` coins4 thomas (john.GetAddress()) 250 | |
//let coins5 = transfer4 |> ``fetch coins of`` thomas | |
let block2 = | |
[coinbase2; transfer3; transfer4] | |
|> ``to new block`` | |
let chained2 = | |
block2 |> ``attach to chain`` BlockChainCheck.EasyWork chain | |
block2.Transactions |> Seq.iter (fun tx -> | |
tracker.NotifyTransaction(tx, chained2, block2) |> ignore) | |
// Check the validity of the chain and the tracker | |
let ``chain still ok`` = chain.ValidateEasy network | |
let ``tracker still ok`` = tracker.Validate() | |
let transactions2 = tracker.GetWalletTransactions chain | |
let ``available coins`` = transactions2.GetSpendableCoins() |> Seq.toList | |
// transactions2.Count | |
// transactions2 |> Seq.map(fun b -> b.Balance, b.Transaction.GetHash()) | |
// |> Seq.toArray | |
// ``available coins`` |> List.map(fun v -> v.Amount) |> List.sum | |
// ``available coins`` | |
// transactions2.Summary.Confirmed | |
// Thomas: 50, John: 250+500+300 | |
``save tracker`` @"c:\tracker.dat" tracker | |
``save chain`` @"c:\chain.dat" chain | |
// let tracker2 = ``load tracker`` @"c:\tracker.dat" | |
// let chain2 = ``load chain`` @"c:\chain.dat" | |
// Some resources: | |
// Article: https://www.codeproject.com/articles/835098/nbitcoin-build-them-all | |
// A video: https://www.youtube.com/watch?v=_160oMzblY8 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment