diff --git a/.gitignore b/.gitignore index 3c4efe2..4b1cec6 100644 --- a/.gitignore +++ b/.gitignore @@ -258,4 +258,7 @@ paket-files/ # Python Tools for Visual Studio (PTVS) __pycache__/ -*.pyc \ No newline at end of file +*.pyc +Samples/DigitalCurrency/node.db + +Samples/DigitalCurrency/node-journal.db diff --git a/NBlockchain.sln b/NBlockchain.sln index 469a82a..968a653 100644 --- a/NBlockchain.sln +++ b/NBlockchain.sln @@ -11,8 +11,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{F160 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{A7AAFCC0-A390-4A51-9CE5-3C7ABCE95A9A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScratchPad", "ScratchPad\ScratchPad.csproj", "{432F4777-5194-4628-A77A-101728E6D427}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Providers", "Providers", "{E090655F-2F94-4839-9BCD-62B4AFA4E3F0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NBlockchain.MongoDB", "Providers\NBlockchain.MongoDB\NBlockchain.MongoDB.csproj", "{D3D3787E-5B04-4A57-8E9B-9D0AB5DBC8D9}" @@ -40,10 +38,6 @@ Global {804876E4-8D7A-4C18-B772-B716849880DA}.Debug|Any CPU.Build.0 = Debug|Any CPU {804876E4-8D7A-4C18-B772-B716849880DA}.Release|Any CPU.ActiveCfg = Release|Any CPU {804876E4-8D7A-4C18-B772-B716849880DA}.Release|Any CPU.Build.0 = Release|Any CPU - {432F4777-5194-4628-A77A-101728E6D427}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {432F4777-5194-4628-A77A-101728E6D427}.Debug|Any CPU.Build.0 = Debug|Any CPU - {432F4777-5194-4628-A77A-101728E6D427}.Release|Any CPU.ActiveCfg = Release|Any CPU - {432F4777-5194-4628-A77A-101728E6D427}.Release|Any CPU.Build.0 = Release|Any CPU {D3D3787E-5B04-4A57-8E9B-9D0AB5DBC8D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D3D3787E-5B04-4A57-8E9B-9D0AB5DBC8D9}.Debug|Any CPU.Build.0 = Debug|Any CPU {D3D3787E-5B04-4A57-8E9B-9D0AB5DBC8D9}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -62,7 +56,6 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {804876E4-8D7A-4C18-B772-B716849880DA} = {F160E2A1-7F39-4223-9D88-DE86EA01E876} - {432F4777-5194-4628-A77A-101728E6D427} = {A7AAFCC0-A390-4A51-9CE5-3C7ABCE95A9A} {D3D3787E-5B04-4A57-8E9B-9D0AB5DBC8D9} = {E090655F-2F94-4839-9BCD-62B4AFA4E3F0} {23039CFC-0063-40CA-B4C3-88C60B08849D} = {A7AAFCC0-A390-4A51-9CE5-3C7ABCE95A9A} {174D35FD-A3CB-4F33-BD1D-13562AC26D33} = {A7AAFCC0-A390-4A51-9CE5-3C7ABCE95A9A} diff --git a/NBlockchain/Interfaces/IAddressEncoder.cs b/NBlockchain/Interfaces/IAddressEncoder.cs index cc3c0e3..a1e838b 100644 --- a/NBlockchain/Interfaces/IAddressEncoder.cs +++ b/NBlockchain/Interfaces/IAddressEncoder.cs @@ -3,7 +3,8 @@ public interface IAddressEncoder { string EncodeAddress(byte[] publicKey, byte type); - byte[] ExtractPublicKey(string address); + byte[] ExtractPublicKeyHash(string address); + byte[] HashPublicKey(byte[] publicKey); bool IsValidAddress(string address); } } \ No newline at end of file diff --git a/NBlockchain/Interfaces/IAsymetricCryptographyService.cs b/NBlockchain/Interfaces/IAsymetricCryptographyService.cs new file mode 100644 index 0000000..e92a8d2 --- /dev/null +++ b/NBlockchain/Interfaces/IAsymetricCryptographyService.cs @@ -0,0 +1,11 @@ +namespace NBlockchain.Interfaces +{ + public interface IAsymetricCryptographyService + { + byte[] BuildPrivateKeyFromPhrase(string phrase); + byte[] GeneratePrivateKey(); + byte[] GetPublicKey(byte[] privateKey); + byte[] Sign(byte[] data, byte[] privateKey); + bool Verify(byte[] data, byte[] sig, byte[] publicKey); + } +} \ No newline at end of file diff --git a/NBlockchain/Interfaces/IBlockBuilder.cs b/NBlockchain/Interfaces/IBlockMiner.cs similarity index 86% rename from NBlockchain/Interfaces/IBlockBuilder.cs rename to NBlockchain/Interfaces/IBlockMiner.cs index 203c5e2..b289073 100644 --- a/NBlockchain/Interfaces/IBlockBuilder.cs +++ b/NBlockchain/Interfaces/IBlockMiner.cs @@ -5,7 +5,7 @@ namespace NBlockchain.Interfaces { - public interface IBlockBuilder + public interface IBlockMiner { void Start(KeyPair builderKeys, bool genesis); void Stop(); diff --git a/NBlockchain/Interfaces/IBlockNotary.cs b/NBlockchain/Interfaces/IBlockNotary.cs deleted file mode 100644 index c2633d4..0000000 --- a/NBlockchain/Interfaces/IBlockNotary.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Threading.Tasks; -using System.Threading; -using NBlockchain.Models; - -namespace NBlockchain.Interfaces -{ - public interface IBlockNotary - { - Task ConfirmBlock(Block block, CancellationToken cancellationToken); - } -} \ No newline at end of file diff --git a/NBlockchain/Interfaces/IBlockReceiver.cs b/NBlockchain/Interfaces/IBlockReceiver.cs deleted file mode 100644 index ad11843..0000000 --- a/NBlockchain/Interfaces/IBlockReceiver.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; -using NBlockchain.Models; - -namespace NBlockchain.Interfaces -{ - public interface IBlockReceiver - { - Task RecieveBlock(Block block); - - Task RecieveTail(Block block); - } - - public enum PeerDataResult { Ignore, Relay, Demerit } -} diff --git a/NBlockchain/Interfaces/IBlockRepository.cs b/NBlockchain/Interfaces/IBlockRepository.cs index 476fa74..d377805 100644 --- a/NBlockchain/Interfaces/IBlockRepository.cs +++ b/NBlockchain/Interfaces/IBlockRepository.cs @@ -1,17 +1,36 @@ using System; using System.Threading.Tasks; using NBlockchain.Models; +using System.Collections.Generic; namespace NBlockchain.Interfaces { public interface IBlockRepository { Task AddBlock(Block block); - Task HaveBlock(byte[] blockId); + Task HavePrimaryBlock(byte[] blockId); + Task HaveSecondaryBlock(byte[] blockId); Task IsEmpty(); - Task GetNewestBlockHeader(); + Task GetBestBlockHeader(); Task GetNextBlock(byte[] prevBlockId); - Task GetAverageBlockTime(DateTime startUtc, DateTime endUtc); + Task GetBlockHeader(byte[] blockId); + Task GetBlock(byte[] blockId); + + Task GetPrimaryHeader(uint height); + Task GetSecondaryHeader(byte[] forkBlockId); + + Task AddSecondaryBlock(Block block); + + Task GetDivergentHeader(byte[] forkTipBlockId); + + Task RewindChain(byte[] blockId); + + Task> GetFork(byte[] forkTipBlockId); + + Task DiscardSecondaryBlock(byte[] blockId); + + Task GetAverageBlockTimeInSecs(DateTime startUtc, DateTime endUtc); + } } \ No newline at end of file diff --git a/NBlockchain/Interfaces/IBlockVerifier.cs b/NBlockchain/Interfaces/IBlockVerifier.cs index 4f562bb..7985318 100644 --- a/NBlockchain/Interfaces/IBlockVerifier.cs +++ b/NBlockchain/Interfaces/IBlockVerifier.cs @@ -1,14 +1,17 @@ using System.Collections.Generic; +using System.Threading.Tasks; using NBlockchain.Models; namespace NBlockchain.Interfaces { public interface IBlockVerifier { - bool Verify(Block block); + Task Verify(Block block); - bool VerifyContentThreshold(ICollection actual, ICollection expected); + Task VerifyTransactions(Block block); - int VerifyTransaction(TransactionEnvelope transaction, ICollection siblings); + Task VerifyBlockRules(Block block, bool tail); + + Task VerifyTransaction(Transaction transaction, ICollection siblings); } } \ No newline at end of file diff --git a/NBlockchain/Interfaces/IBlockbaseTransactionBuilder.cs b/NBlockchain/Interfaces/IBlockbaseTransactionBuilder.cs index 94f0f35..31042af 100644 --- a/NBlockchain/Interfaces/IBlockbaseTransactionBuilder.cs +++ b/NBlockchain/Interfaces/IBlockbaseTransactionBuilder.cs @@ -1,10 +1,11 @@ using System.Collections.Generic; +using System.Threading.Tasks; using NBlockchain.Models; namespace NBlockchain.Interfaces { public interface IBlockbaseTransactionBuilder { - TransactionEnvelope Build(KeyPair builderKeys, ICollection transactions); + Task Build(KeyPair builderKeys, ICollection transactions); } } \ No newline at end of file diff --git a/NBlockchain/Interfaces/IBlockchainNode.cs b/NBlockchain/Interfaces/IBlockchainNode.cs new file mode 100644 index 0000000..8729467 --- /dev/null +++ b/NBlockchain/Interfaces/IBlockchainNode.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using NBlockchain.Models; + +namespace NBlockchain.Interfaces +{ + public interface IBlockchainNode + { + Task SendTransaction(Transaction transaction); + Task RecieveBlock(Block block); + Task RecieveTransaction(Transaction transaction); + } +} \ No newline at end of file diff --git a/NBlockchain/Interfaces/IConsensusMethod.cs b/NBlockchain/Interfaces/IConsensusMethod.cs new file mode 100644 index 0000000..22ad708 --- /dev/null +++ b/NBlockchain/Interfaces/IConsensusMethod.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using System.Threading; +using NBlockchain.Models; + +namespace NBlockchain.Interfaces +{ + public interface IConsensusMethod + { + Task BuildConsensus(Block block, CancellationToken cancellationToken); + bool VerifyConsensus(Block block); + } +} \ No newline at end of file diff --git a/NBlockchain/Interfaces/IExpectedBlockList.cs b/NBlockchain/Interfaces/IExpectedBlockList.cs new file mode 100644 index 0000000..9ac074d --- /dev/null +++ b/NBlockchain/Interfaces/IExpectedBlockList.cs @@ -0,0 +1,9 @@ +namespace NBlockchain.Interfaces +{ + public interface IExpectedBlockList + { + void Confirm(byte[] previousId); + void ExpectNext(byte[] previousId); + bool IsExpected(byte[] previousId); + } +} \ No newline at end of file diff --git a/NBlockchain/Interfaces/IForkRebaser.cs b/NBlockchain/Interfaces/IForkRebaser.cs new file mode 100644 index 0000000..629f551 --- /dev/null +++ b/NBlockchain/Interfaces/IForkRebaser.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using NBlockchain.Models; + +namespace NBlockchain.Interfaces +{ + public interface IForkRebaser + { + Task FindKnownForkbase(byte[] forkTipId); + Task> RebaseChain(byte[] divergentId, byte[] targetTipId); + } +} \ No newline at end of file diff --git a/NBlockchain/Interfaces/IInstructionRepository.cs b/NBlockchain/Interfaces/IInstructionRepository.cs new file mode 100644 index 0000000..edf3506 --- /dev/null +++ b/NBlockchain/Interfaces/IInstructionRepository.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace NBlockchain.Interfaces +{ + public interface IInstructionRepository + { + Task HaveInstruction(byte[] instructionId); + } +} diff --git a/NBlockchain/Interfaces/INatTraversal.cs b/NBlockchain/Interfaces/INatTraversal.cs new file mode 100644 index 0000000..15304c6 --- /dev/null +++ b/NBlockchain/Interfaces/INatTraversal.cs @@ -0,0 +1,12 @@ +using System.Net; +using System.Threading.Tasks; +using NBlockchain.Services.Net; +using Open.Nat; + +namespace NBlockchain.Interfaces +{ + public interface INatTraversal + { + string ConfigureNatTraversal(IPAddress ownAddress, int internalPort); + } +} \ No newline at end of file diff --git a/NBlockchain/Interfaces/INetworkParameters.cs b/NBlockchain/Interfaces/INetworkParameters.cs index 9bb8961..221d963 100644 --- a/NBlockchain/Interfaces/INetworkParameters.cs +++ b/NBlockchain/Interfaces/INetworkParameters.cs @@ -6,6 +6,5 @@ public interface INetworkParameters { TimeSpan BlockTime { get; } uint HeaderVersion { get; } - decimal ExpectedContentThreshold { get; } } } \ No newline at end of file diff --git a/NBlockchain/Interfaces/INodeHost.cs b/NBlockchain/Interfaces/INodeHost.cs deleted file mode 100644 index c1ab38b..0000000 --- a/NBlockchain/Interfaces/INodeHost.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Threading.Tasks; -using NBlockchain.Models; - -namespace NBlockchain.Interfaces -{ - public interface INodeHost : IBlockReceiver, ITransactionReceiver - { - Task SendTransaction(TransactionEnvelope transaction); - } -} \ No newline at end of file diff --git a/NBlockchain/Interfaces/IPeerNetwork.cs b/NBlockchain/Interfaces/IPeerNetwork.cs index fbead44..d99059e 100644 --- a/NBlockchain/Interfaces/IPeerNetwork.cs +++ b/NBlockchain/Interfaces/IPeerNetwork.cs @@ -12,19 +12,36 @@ public interface IPeerNetwork void BroadcastTail(Block block); - void BroadcastTransaction(TransactionEnvelope transaction); + void BroadcastTransaction(Transaction transaction); void RequestNextBlock(byte[] blockId); - - void RegisterBlockReceiver(IBlockReceiver blockReceiver); - void RegisterTransactionReceiver(ITransactionReceiver transactionReciever); + void RequestBlock(byte[] blockId); - void DiscoverPeers(); + void RequestBlockByHeight(uint height); + + Task DiscoverPeers(); void Open(); void Close(); + ICollection GetPeersIn(); + ICollection GetPeersOut(); + + } + + public class ConnectedPeer + { + public Guid NodeId { get; set; } + public string Address { get; set; } + public DateTime LastContact { get; set; } + + public ConnectedPeer(Guid nodeId, string address) + { + NodeId = nodeId; + Address = address; + LastContact = DateTime.Now; + } } } diff --git a/NBlockchain/Interfaces/IPendingTransactionList.cs b/NBlockchain/Interfaces/IPendingTransactionList.cs deleted file mode 100644 index 92e1260..0000000 --- a/NBlockchain/Interfaces/IPendingTransactionList.cs +++ /dev/null @@ -1,20 +0,0 @@ -using NBlockchain.Models; -using System; -using System.Collections.Generic; -using System.Text; - -namespace NBlockchain.Interfaces -{ - public interface IPendingTransactionList - { - - event EventHandler Changed; - - bool Add(TransactionEnvelope txn); - - void Remove(ICollection toRemove); - - ICollection Get { get; } - - } -} diff --git a/NBlockchain/Interfaces/IProvideUpnpDevice.cs b/NBlockchain/Interfaces/IProvideUpnpDevice.cs new file mode 100644 index 0000000..c6971b2 --- /dev/null +++ b/NBlockchain/Interfaces/IProvideUpnpDevice.cs @@ -0,0 +1,44 @@ +using System.Collections; +using System.Collections.Generic; +using System.Net; +using Open.Nat; + +namespace NBlockchain.Interfaces +{ + public interface IProvideUpnpDevice + { + IPAddress GetExternalIp(); + void CreateMapping(int internalPort, int externalPort, string mappingIdentifier); + IEnumerable GetAllMappings(); + } + + public class OpenNatUpnpProvider : IProvideUpnpDevice + { + private readonly NatDevice _device; + + public OpenNatUpnpProvider() + { + var devicetask = new NatDiscoverer().DiscoverDeviceAsync(); + devicetask.Wait(); + _device = devicetask.Result; + } + public IPAddress GetExternalIp() + { + var ipTask = _device.GetExternalIPAsync(); + ipTask.Wait(); + return ipTask.Result; + } + + public void CreateMapping(int internalPort, int externalPort, string mappingIdentifier) + { + _device.CreatePortMapAsync(new Mapping(Protocol.Tcp, internalPort, externalPort, mappingIdentifier)); + } + + public IEnumerable GetAllMappings() + { + var mappingsTask = _device.GetAllMappingsAsync(); + mappingsTask.Wait(); + return mappingsTask.Result; + } + } +} \ No newline at end of file diff --git a/NBlockchain/Interfaces/IReceiver.cs b/NBlockchain/Interfaces/IReceiver.cs new file mode 100644 index 0000000..9860459 --- /dev/null +++ b/NBlockchain/Interfaces/IReceiver.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using NBlockchain.Models; + +namespace NBlockchain.Interfaces +{ + public interface IReceiver + { + Task RecieveBlock(Block block); + Task RecieveTransaction(Transaction transaction); + event ReceiveBlock OnReceiveBlock; + event RecieveTransaction OnRecieveTransaction; + } + + public enum PeerDataResult { Ignore, Relay, Demerit } + + public delegate Task ReceiveBlock(Block block); + public delegate Task RecieveTransaction(Transaction transaction); +} diff --git a/NBlockchain/Interfaces/ISignatureService.cs b/NBlockchain/Interfaces/ISignatureService.cs index 68fd5aa..736cffa 100644 --- a/NBlockchain/Interfaces/ISignatureService.cs +++ b/NBlockchain/Interfaces/ISignatureService.cs @@ -5,7 +5,8 @@ namespace NBlockchain.Interfaces public interface ISignatureService { KeyPair GenerateKeyPair(); - void SignTransaction(TransactionEnvelope transaction, byte[] privateKey); - bool VerifyTransaction(TransactionEnvelope transaction); + KeyPair GetKeyPairFromPhrase(string phrase); + void SignInstruction(Instruction instruction, byte[] privateKey); + bool VerifyInstruction(Instruction instruction); } } \ No newline at end of file diff --git a/NBlockchain/Interfaces/ITransactionBuilder.cs b/NBlockchain/Interfaces/ITransactionBuilder.cs new file mode 100644 index 0000000..f09c3bd --- /dev/null +++ b/NBlockchain/Interfaces/ITransactionBuilder.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using NBlockchain.Models; + +namespace NBlockchain.Interfaces +{ + public interface ITransactionBuilder + { + Task Build(ICollection instructions); + } +} \ No newline at end of file diff --git a/NBlockchain/Interfaces/ITransactionKeyResolver.cs b/NBlockchain/Interfaces/ITransactionKeyResolver.cs index 15bcdfa..ac5a204 100644 --- a/NBlockchain/Interfaces/ITransactionKeyResolver.cs +++ b/NBlockchain/Interfaces/ITransactionKeyResolver.cs @@ -1,9 +1,10 @@ -using NBlockchain.Models; +using System.Threading.Tasks; +using NBlockchain.Models; namespace NBlockchain.Interfaces { public interface ITransactionKeyResolver { - byte[] ResolveKey(TransactionEnvelope txn); + Task ResolveKey(Transaction txn); } } \ No newline at end of file diff --git a/NBlockchain/Interfaces/IUnconfirmedTransactionPool.cs b/NBlockchain/Interfaces/IUnconfirmedTransactionPool.cs new file mode 100644 index 0000000..ae6f7e5 --- /dev/null +++ b/NBlockchain/Interfaces/IUnconfirmedTransactionPool.cs @@ -0,0 +1,20 @@ +using NBlockchain.Models; +using System; +using System.Collections.Generic; +using System.Text; + +namespace NBlockchain.Interfaces +{ + public interface IUnconfirmedTransactionPool + { + + event EventHandler Changed; + + bool Add(Transaction txn); + + void Remove(ICollection toRemove); + + ICollection Get { get; } + + } +} diff --git a/NBlockchain/Models/Block.cs b/NBlockchain/Models/Block.cs index 44798f8..e013145 100644 --- a/NBlockchain/Models/Block.cs +++ b/NBlockchain/Models/Block.cs @@ -8,8 +8,8 @@ public class Block { public static byte[] HeadKey = new byte[] { 0x0 }; - public BlockHeader Header { get; set; } - public ICollection Transactions { get; set; } = new HashSet(); + public BlockHeader Header { get; set; } + public ICollection Transactions { get; set; } = new HashSet(); public MerkleNode MerkleRootNode { get; set; } public Block() diff --git a/NBlockchain/Models/BlockchainOptions.cs b/NBlockchain/Models/BlockchainOptions.cs index 608b0ce..5bbd15f 100644 --- a/NBlockchain/Models/BlockchainOptions.cs +++ b/NBlockchain/Models/BlockchainOptions.cs @@ -10,6 +10,9 @@ using NBlockchain.Services.Hashers; using NBlockchain.Services.Net; using NBlockchain.Services.PeerDiscovery; +using NBlockchain.Rules; +using NBlockchain.Services.NatTraversal; +using Org.BouncyCastle.Bcpg.OpenPgp; namespace NBlockchain.Models { @@ -36,10 +39,10 @@ public void UseHasher() Services.AddTransient(typeof(IHasher), typeof(T)); } - public void UseBlockNotary() - where T : IBlockNotary + public void UseConsensusMethod() + where T : IConsensusMethod { - Services.AddTransient(typeof(IBlockNotary), typeof(T)); + Services.AddTransient(typeof(IConsensusMethod), typeof(T)); } public void UseSignatureService() @@ -67,7 +70,31 @@ public void UseBlockRepository(Func factory) public void UseTcpPeerNetwork(uint port) { - Services.AddSingleton(sp => new TcpPeerNetwork(port, sp.GetService(), sp.GetServices(), sp.GetService(), sp.GetService())); + Services.AddSingleton(sp => new TcpPeerNetwork(port, sp.GetService(), sp.GetService(), sp.GetServices(), sp.GetService(), sp.GetService(), sp.GetService(), sp.GetService())); + } + + public void UseStaticNatTraversal(uint staticPort) + { + Services.AddSingleton(sp => new StaticPortForwarding((int)staticPort, sp.GetService())); + } + + public void UseNoNatTraversal() + { + Services.AddSingleton(); + } + + public void UseUpnpAutoNatTraversal(string mappingIdentifier) + { + Services.AddSingleton(sp => new UpnpAutodetectPortForwarding(mappingIdentifier, sp.GetService(), sp.GetService())); + } + + public void UsePnpStaticNatTraversal(uint staticPort, string mappingIdentifier) + { + Services.AddSingleton(sp => new UpnpStaticPortForwarding(mappingIdentifier, (int)staticPort, sp.GetService(), sp.GetService())); + } + public void UseDataConnection(string connectionString) + { + Services.AddSingleton(sp => new DataConnection(connectionString)); } public void UseParameters() @@ -87,6 +114,12 @@ public void AddTransactionRule() Services.AddTransient(typeof(ITransactionRule), typeof(T)); } + public void AddBlockRule() + where T : IBlockRule + { + Services.AddTransient(typeof(IBlockRule), typeof(T)); + } + public void AddPeerDiscovery() where T : IPeerDiscoveryService { @@ -98,64 +131,77 @@ public void AddPeerDiscovery(Func facto Services.AddTransient(factory); } + public void AddContentThresholdBlockRule(decimal threshold) + { + Services.AddTransient(typeof(IBlockRule), sp => new BlockContentThresholdRule(sp.GetService(), threshold)); + } + public void UseMulticastDiscovery(string serviceId, string multicastAddress, int port) { Services.AddTransient(sp => new MulticastDiscovery(serviceId, multicastAddress, port, sp.GetService(), sp.GetService())); } - public void UseTransactionRepository() - where TImplementation : TransactionRepository, TService + public void UseInstructionRepository() + where TImplementation : InstructionRepository, TService where TService : class { Services.AddTransient(); } - public void AddTransactionType() + public void AddInstructionType() { - var attr = typeof(T).GetTypeInfo().GetCustomAttribute(); + var attr = typeof(T).GetTypeInfo().GetCustomAttribute(); if (attr == null) - throw new NotSupportedException("Missing TransactionTypeAttribute"); + throw new NotSupportedException("Missing InstructionTypeAttribute"); - Services.AddSingleton(new ValidTransactionType(attr.TypeId, typeof(T))); + Services.AddSingleton(new ValidInstructionType(attr.TypeIdentifier, typeof(T))); } internal void FillDefaults() { AddDefault(ServiceLifetime.Singleton, x => new StaticNetworkParameters() { - BlockTime = TimeSpan.FromMinutes(1), - Difficulty = 250, - HeaderVersion = 1, - ExpectedContentThreshold = 0.8m + BlockTime = TimeSpan.FromMinutes(1), + HeaderVersion = 1 }); AddDefault(ServiceLifetime.Transient); + + AddDefault(ServiceLifetime.Transient); + AddDefault(ServiceLifetime.Transient); AddDefault(ServiceLifetime.Transient); AddDefault(ServiceLifetime.Transient); - AddDefault(ServiceLifetime.Transient); + AddDefault(ServiceLifetime.Transient); AddDefault(ServiceLifetime.Transient); - AddDefault(ServiceLifetime.Singleton); + AddDefault(ServiceLifetime.Singleton); AddDefault(ServiceLifetime.Transient); AddDefault(ServiceLifetime.Transient); AddDefault(ServiceLifetime.Singleton); + AddDefault(ServiceLifetime.Singleton); AddDefault(ServiceLifetime.Singleton); AddDefault(ServiceLifetime.Singleton); - AddDefault(ServiceLifetime.Singleton); + AddDefault(ServiceLifetime.Singleton); + AddDefault(ServiceLifetime.Singleton); + //AddDefault(ServiceLifetime.Singleton); + + //AddDefault(ServiceLifetime.Singleton); - AddDefault(ServiceLifetime.Singleton); - AddDefault(ServiceLifetime.Singleton, sp => sp.GetService()); - AddDefault(ServiceLifetime.Singleton, sp => sp.GetService()); + AddDefault(ServiceLifetime.Singleton); + AddDefault(ServiceLifetime.Singleton); + AddDefault(ServiceLifetime.Singleton); AddDefault(ServiceLifetime.Singleton); AddDefault(ServiceLifetime.Singleton); - //AddDefault(ServiceLifetime.Singleton); - + AddDefault(ServiceLifetime.Singleton); - AddDefault(ServiceLifetime.Singleton); + AddDefault(ServiceLifetime.Singleton); + AddDefault(ServiceLifetime.Singleton); + AddDefault(ServiceLifetime.Singleton); + AddDefault(ServiceLifetime.Singleton); } diff --git a/NBlockchain/Models/Instruction.cs b/NBlockchain/Models/Instruction.cs new file mode 100644 index 0000000..63239f8 --- /dev/null +++ b/NBlockchain/Models/Instruction.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Linq; + +namespace NBlockchain.Models +{ + public abstract class Instruction + { + public byte[] InstructionId { get; set; } + + public byte[] OriginKey { get; set; } + + public byte[] PublicKey { get; set; } + + public byte[] Signature { get; set; } + + public abstract ICollection ExtractSignableElements(); + } +} diff --git a/NBlockchain/Models/InstructionTypeAttribute.cs b/NBlockchain/Models/InstructionTypeAttribute.cs new file mode 100644 index 0000000..9864cf8 --- /dev/null +++ b/NBlockchain/Models/InstructionTypeAttribute.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace NBlockchain.Models +{ + public class InstructionTypeAttribute : Attribute + { + public string TypeIdentifier { get; } + + public InstructionTypeAttribute(string typeIdentifier) + { + TypeIdentifier = typeIdentifier; + } + } +} diff --git a/NBlockchain/Models/KnownPeer.cs b/NBlockchain/Models/KnownPeer.cs index 826498e..1b72b83 100644 --- a/NBlockchain/Models/KnownPeer.cs +++ b/NBlockchain/Models/KnownPeer.cs @@ -8,6 +8,8 @@ public class KnownPeer { public string ConnectionString { get; set; } public DateTime LastContact { get; set; } - + public bool IsSelf { get; set; } + public Guid NodeId { get; set; } + public bool Unreachable { get; set; } } } diff --git a/NBlockchain/Models/PersistedEntity.cs b/NBlockchain/Models/PersistedEntity.cs deleted file mode 100644 index 8206c74..0000000 --- a/NBlockchain/Models/PersistedEntity.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace NBlockchain.Models -{ - public class PersistedEntity - { - public TKey Id { get; set; } - public TEntity Entity { get; set; } - - public PersistedEntity() - { - } - - public PersistedEntity(TEntity entity) - { - Entity = entity; - } - } -} diff --git a/NBlockchain/Models/StaticNetworkParameters.cs b/NBlockchain/Models/StaticNetworkParameters.cs index 89d4e88..abafc07 100644 --- a/NBlockchain/Models/StaticNetworkParameters.cs +++ b/NBlockchain/Models/StaticNetworkParameters.cs @@ -7,9 +7,7 @@ namespace NBlockchain.Models { public class StaticNetworkParameters : INetworkParameters { - public TimeSpan BlockTime { get; set; } - public uint Difficulty { get; set; } + public TimeSpan BlockTime { get; set; } public uint HeaderVersion { get; set; } - public decimal ExpectedContentThreshold { get; set; } } } diff --git a/NBlockchain/Models/Transaction.cs b/NBlockchain/Models/Transaction.cs new file mode 100644 index 0000000..1926eed --- /dev/null +++ b/NBlockchain/Models/Transaction.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; +using Newtonsoft.Json.Linq; + +namespace NBlockchain.Models +{ + public class Transaction + { + public byte[] TransactionId { get; set; } + + public ICollection Instructions { get; set; } = new HashSet(); + + public Transaction() + { + } + + public Transaction(ICollection instructions) + { + Instructions = instructions; + } + } +} diff --git a/NBlockchain/Models/TransactionBucket.cs b/NBlockchain/Models/TransactionBucket.cs deleted file mode 100644 index bac2837..0000000 --- a/NBlockchain/Models/TransactionBucket.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using NBlockchain.Services; - -namespace NBlockchain.Models -{ - public class TransactionBucket - { - private readonly AutoResetEvent _resetEvent = new AutoResetEvent(true); - private readonly Dictionary> _buckets = new Dictionary>(); - private readonly Dictionary _txns = new Dictionary(new ByteArrayEqualityComparer()); - private readonly IEqualityComparer _byteArrayEqualityComparer = new ByteArrayEqualityComparer(); - - public bool AddTransaction(byte[] txnId, TransactionEnvelope txn, uint height) - { - _resetEvent.WaitOne(); - try - { - EnsureKey(height); - if (_buckets[height].Add(txnId)) - { - _txns[txnId] = txn; - return true; - } - return false; - } - finally - { - _resetEvent.Set(); - } - } - - public ICollection GetBucket(uint height) - { - _resetEvent.WaitOne(); - try - { - EnsureKey(height); - return _buckets[height]; - } - finally - { - _resetEvent.Set(); - } - } - - public void Shift(uint height, ICollection toRemove) - { - _resetEvent.WaitOne(); - try - { - EnsureKey(height); - EnsureKey(height + 1); - - foreach (var item in _buckets[height].Where(x => !toRemove.Contains(x, _byteArrayEqualityComparer))) - _buckets[height + 1].Add(item); - - _buckets.Remove(height); - - foreach (var txnId in toRemove) - _txns.Remove(txnId); - } - finally - { - _resetEvent.Set(); - } - } - - public ICollection GetTransactions(uint height) - { - _resetEvent.WaitOne(); - try - { - if (_buckets.ContainsKey(height)) - { - return _txns.Where(x => _buckets[height].Contains(x.Key, _byteArrayEqualityComparer)) - .Select(x => x.Value) - .ToList(); - } - return new List(); - } - finally - { - _resetEvent.Set(); - } - } - - private void EnsureKey(uint height) - { - if (!_buckets.ContainsKey(height)) - _buckets.Add(height, new HashSet(_byteArrayEqualityComparer)); - } - - } -} diff --git a/NBlockchain/Models/TransactionEnvelope.cs b/NBlockchain/Models/TransactionEnvelope.cs deleted file mode 100644 index d2bc6d7..0000000 --- a/NBlockchain/Models/TransactionEnvelope.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Newtonsoft.Json.Linq; - -namespace NBlockchain.Models -{ - public class TransactionEnvelope - { - public string TransactionType { get; set; } - - public Guid OriginKey{ get; set; } - - public string Originator { get; set; } - - public byte[] Signature { get; set; } - - public object Transaction { get; set; } - - public TransactionEnvelope() - { - } - - public TransactionEnvelope(object transaction) - { - Transaction = transaction; - } - - } -} diff --git a/NBlockchain/Models/TransactionTypeAttribute.cs b/NBlockchain/Models/TransactionTypeAttribute.cs deleted file mode 100644 index eab8c58..0000000 --- a/NBlockchain/Models/TransactionTypeAttribute.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace NBlockchain.Models -{ - public class TransactionTypeAttribute : Attribute - { - public string TypeId { get; } - - public TransactionTypeAttribute(string typeId) - { - TypeId = typeId; - } - } -} diff --git a/NBlockchain/Models/ValidTransactionType.cs b/NBlockchain/Models/ValidInstructionType.cs similarity index 53% rename from NBlockchain/Models/ValidTransactionType.cs rename to NBlockchain/Models/ValidInstructionType.cs index 7a8c4f6..6617baa 100644 --- a/NBlockchain/Models/ValidTransactionType.cs +++ b/NBlockchain/Models/ValidInstructionType.cs @@ -4,15 +4,15 @@ namespace NBlockchain.Models { - public class ValidTransactionType + public class ValidInstructionType { - public string TransactionType { get; private set; } + public string InstructionType { get; private set; } public Type ClassType { get; private set; } - public ValidTransactionType(string transactionType, Type classType) + public ValidInstructionType(string instructionType, Type classType) { - TransactionType = transactionType; + InstructionType = instructionType; ClassType = classType; } } diff --git a/NBlockchain/NBlockchain.csproj b/NBlockchain/NBlockchain.csproj index a6e025b..e6887b9 100644 --- a/NBlockchain/NBlockchain.csproj +++ b/NBlockchain/NBlockchain.csproj @@ -2,9 +2,9 @@ netstandard2.0 - 0.1.0-alpha - 0.1.0.0 - 0.1.0.0 + 0.6.0-alpha + 0.6.0.0 + 0.6.0.0 https://github.com/danielgerlag/NBlockchain https://github.com/danielgerlag/NBlockchain.git git @@ -20,12 +20,13 @@ + - + @@ -36,6 +37,11 @@ + + + + + \ No newline at end of file diff --git a/NBlockchain/Rules/BlockContentThresholdRule.cs b/NBlockchain/Rules/BlockContentThresholdRule.cs new file mode 100644 index 0000000..dbc9e9f --- /dev/null +++ b/NBlockchain/Rules/BlockContentThresholdRule.cs @@ -0,0 +1,35 @@ +using NBlockchain.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using NBlockchain.Models; + +namespace NBlockchain.Rules +{ + public class BlockContentThresholdRule : IBlockRule + { + private readonly IUnconfirmedTransactionPool _unconfirmedTransactions; + private readonly decimal _threshold; + + public bool TailRule => true; + + public BlockContentThresholdRule(IUnconfirmedTransactionPool unconfirmedTransactions, decimal threshold) + { + _unconfirmedTransactions = unconfirmedTransactions; + _threshold = threshold; + } + + public Task Validate(Block block) + { + var expected = _unconfirmedTransactions.Get; + if (expected.Count == 0) + return Task.FromResult(true); + + var count = expected.Count(txn => block.Transactions.Any(actual => actual.TransactionId.SequenceEqual(txn.TransactionId))); + var ratio = (decimal)count / (decimal)expected.Count; + return Task.FromResult(ratio >= _threshold); + } + + } +} diff --git a/NBlockchain/Interfaces/ITransactionReceiver.cs b/NBlockchain/Rules/IBlockRule.cs similarity index 58% rename from NBlockchain/Interfaces/ITransactionReceiver.cs rename to NBlockchain/Rules/IBlockRule.cs index 1463408..8fde0a4 100644 --- a/NBlockchain/Interfaces/ITransactionReceiver.cs +++ b/NBlockchain/Rules/IBlockRule.cs @@ -6,8 +6,9 @@ namespace NBlockchain.Interfaces { - public interface ITransactionReceiver + public interface IBlockRule { - Task RecieveTransaction(TransactionEnvelope transaction); + Task Validate(Block block); + bool TailRule { get; } } } diff --git a/NBlockchain/Interfaces/ITransactionRule.cs b/NBlockchain/Rules/ITransactionRule.cs similarity index 60% rename from NBlockchain/Interfaces/ITransactionRule.cs rename to NBlockchain/Rules/ITransactionRule.cs index 800b312..3839cfc 100644 --- a/NBlockchain/Interfaces/ITransactionRule.cs +++ b/NBlockchain/Rules/ITransactionRule.cs @@ -8,7 +8,6 @@ namespace NBlockchain.Interfaces { public interface ITransactionRule { - string TransactionType { get; } - int Validate(TransactionEnvelope transaction, ICollection siblings); + int Validate(Transaction transaction, ICollection siblings); } } diff --git a/NBlockchain/Services/AddressEncoder.cs b/NBlockchain/Services/AddressEncoder.cs index 028e05a..7ddb652 100644 --- a/NBlockchain/Services/AddressEncoder.cs +++ b/NBlockchain/Services/AddressEncoder.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Cryptography; using NBlockchain.Interfaces; using Wiry.Base32; @@ -8,6 +9,7 @@ namespace NBlockchain.Services { public class AddressEncoder : IAddressEncoder { + private const int ChecksumLength = 4; public AddressEncoder() { } @@ -15,40 +17,38 @@ public AddressEncoder() public string EncodeAddress(byte[] publicKey, byte type) { var result = new List {type}; - result.AddRange(publicKey); - result.Add(CalculateCheckSum(result)); + result.AddRange(HashPublicKey(publicKey)); + result.AddRange(CalculateCheckSum(result.ToArray())); return Base32Encoding.ZBase32.GetString(result.ToArray()); } - public byte[] ExtractPublicKey(string address) + public byte[] ExtractPublicKeyHash(string address) { var raw = Base32Encoding.ZBase32.ToBytes(address); - return raw.Skip(1).Take(raw.Length - 2).ToArray(); + return raw.Skip(1).Take(raw.Length - (1 + ChecksumLength)).ToArray(); } + public byte[] HashPublicKey(byte[] publicKey) + { + using (var hasher = SHA1.Create()) + { + return hasher.ComputeHash(publicKey); + } + } + public bool IsValidAddress(string address) { var raw = Base32Encoding.ZBase32.ToBytes(address); - return (raw.Last() == CalculateCheckSum(raw.Take(raw.Length - 1))); + return (raw.Skip(raw.Count() - ChecksumLength).SequenceEqual(CalculateCheckSum(raw.Take(raw.Count() - ChecksumLength).ToArray()))); } - private byte CalculateCheckSum(IEnumerable data) + private static byte[] CalculateCheckSum(byte[] data) { - byte result = 0; - var odd = true; - foreach (var item in data) + using (var hasher = SHA256.Create()) { - if (odd) - result += item; - else - result *= item; - - result++; - odd = !odd; + return hasher.ComputeHash(data).Take(ChecksumLength).ToArray(); } - - return result; } } } diff --git a/NBlockchain/Services/AsymetricCryptographyService.cs b/NBlockchain/Services/AsymetricCryptographyService.cs new file mode 100644 index 0000000..9d68e2f --- /dev/null +++ b/NBlockchain/Services/AsymetricCryptographyService.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Text; +using NBlockchain.Interfaces; +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.Nist; +using Org.BouncyCastle.Asn1.Sec; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Security; + +namespace NBlockchain.Services +{ + public class AsymetricCryptographyService : IAsymetricCryptographyService + { + private readonly DerObjectIdentifier _curveId = SecObjectIdentifiers.SecP256r1; + + public byte[] GeneratePrivateKey() + { + return SecureRandom.GetNextBytes(SecureRandom.GetInstance("SHA256PRNG"), 32); + } + + public byte[] BuildPrivateKeyFromPhrase(string phrase) + { + using (var hasher = System.Security.Cryptography.SHA256.Create()) + { + var privateKey = hasher.ComputeHash(Encoding.Unicode.GetBytes(phrase)); + return privateKey; + } + } + + public byte[] GetPublicKey(byte[] privateKey) + { + var privKeyInt = new BigInteger(privateKey); + var parameters = NistNamedCurves.GetByOid(_curveId); + var qa = parameters.G.Multiply(privKeyInt); + + return qa.GetEncoded(); + } + + public byte[] Sign(byte[] data, byte[] privateKey) + { + var cp = GetPrivateKeyParameters(privateKey); + var signer = SignerUtilities.GetSigner("ECDSA"); + signer.Init(true, cp); + signer.BlockUpdate(data, 0, data.Length); + return signer.GenerateSignature(); + } + + public bool Verify(byte[] data, byte[] sig, byte[] publicKey) + { + var cp = GetPublicKeyParams(publicKey); + var signer = SignerUtilities.GetSigner("ECDSA"); + signer.Init(false, cp); + signer.BlockUpdate(data, 0, data.Length); + return signer.VerifySignature(sig); + } + + private ECPrivateKeyParameters GetPrivateKeyParameters(byte[] privateKey) + { + var parameters = NistNamedCurves.GetByOid(_curveId); + var privKeyInt = new BigInteger(privateKey); + var dp = new ECDomainParameters(parameters.Curve, parameters.G, parameters.N); + return new ECPrivateKeyParameters("ECDSA", privKeyInt, dp); + } + + private ECPublicKeyParameters GetPublicKeyParams(byte[] publicKey) + { + var parameters = NistNamedCurves.GetByOid(_curveId); + var dp = new ECDomainParameters(parameters.Curve, parameters.G, parameters.N); + var q = parameters.Curve.DecodePoint(publicKey); + return new ECPublicKeyParameters("ECDSA", q, dp); + } + } +} diff --git a/NBlockchain/Services/BlockBuilder.cs b/NBlockchain/Services/BlockMiner.cs similarity index 67% rename from NBlockchain/Services/BlockBuilder.cs rename to NBlockchain/Services/BlockMiner.cs index e7beb95..c1a3fd2 100644 --- a/NBlockchain/Services/BlockBuilder.cs +++ b/NBlockchain/Services/BlockMiner.cs @@ -13,19 +13,19 @@ namespace NBlockchain.Services { - public class BlockBuilder : IBlockBuilder + public class BlockMiner : IBlockMiner { private readonly INetworkParameters _networkParameters; private readonly ITransactionKeyResolver _transactionKeyResolver; private readonly IMerkleTreeBuilder _merkleTreeBuilder; private readonly IBlockbaseTransactionBuilder _blockbaseBuilder; - private readonly IBlockNotary _blockNotary; + private readonly IConsensusMethod _consensusMethod; private readonly ILogger _logger; private readonly AutoResetEvent _resetEvent = new AutoResetEvent(true); - private readonly IPendingTransactionList _pendingTransactionList; + private readonly IUnconfirmedTransactionPool _unconfirmedTransactionPool; private readonly IBlockRepository _blockRepository; private readonly IPeerNetwork _peerNetwork; - private readonly IBlockReceiver _blockReciever; + private readonly IReceiver _blockReciever; private readonly IDifficultyCalculator _difficultyCalculator; private KeyPair _builderKeys; @@ -36,20 +36,20 @@ public class BlockBuilder : IBlockBuilder private CancellationTokenSource _blockCancelToken; - public BlockBuilder(ITransactionKeyResolver transactionKeyResolver, IMerkleTreeBuilder merkleTreeBuilder, INetworkParameters networkParameters, IBlockbaseTransactionBuilder blockbaseBuilder, IPeerNetwork peerNetwork, IBlockNotary blockNotary, IPendingTransactionList pendingTransactionList, IBlockRepository blockRepository, IBlockReceiver blockReciever, IDifficultyCalculator difficultyCalculator, ILoggerFactory loggerFactory) + public BlockMiner(ITransactionKeyResolver transactionKeyResolver, IMerkleTreeBuilder merkleTreeBuilder, INetworkParameters networkParameters, IBlockbaseTransactionBuilder blockbaseBuilder, IPeerNetwork peerNetwork, IConsensusMethod consensusMethod, IUnconfirmedTransactionPool unconfirmedTransactionPool, IBlockRepository blockRepository, IReceiver blockReciever, IDifficultyCalculator difficultyCalculator, ILoggerFactory loggerFactory) { _networkParameters = networkParameters; _peerNetwork= peerNetwork; _blockbaseBuilder = blockbaseBuilder; _blockReciever = blockReciever; - _blockNotary = blockNotary; + _consensusMethod = consensusMethod; _difficultyCalculator = difficultyCalculator; _transactionKeyResolver = transactionKeyResolver; _merkleTreeBuilder = merkleTreeBuilder; - _logger = loggerFactory.CreateLogger(); + _logger = loggerFactory.CreateLogger(); _blockRepository = blockRepository; - _pendingTransactionList = pendingTransactionList; - _pendingTransactionList.Changed += PendingTransactionList_Changed; + _unconfirmedTransactionPool = unconfirmedTransactionPool; + _unconfirmedTransactionPool.Changed += UnconfirmedTransactionPoolChanged; } @@ -58,7 +58,7 @@ public void Start(KeyPair builderKeys, bool genesis) _builderKeys = builderKeys; _buildGenesis = genesis; _buildCancelToken = new CancellationTokenSource(); - _buildTask = Task.Factory.StartNew(BuildTask); + _buildTask = Task.Factory.StartNew(Mine); } public void Stop() @@ -66,12 +66,12 @@ public void Stop() _buildCancelToken.Cancel(); } - private async void BuildTask() + private async void Mine() { while (!_buildCancelToken.IsCancellationRequested) { _blockCancelToken = new CancellationTokenSource(); - var prevHeader = await _blockRepository.GetNewestBlockHeader(); + var prevHeader = await _blockRepository.GetBestBlockHeader(); if (prevHeader == null) { if (!_buildGenesis) @@ -80,7 +80,7 @@ private async void BuildTask() continue; } prevHeader = new BlockHeader() { BlockId = Block.HeadKey, Height = 0, Timestamp = DateTime.UtcNow.Ticks }; - _logger.LogInformation($"Building genesis block"); + _logger.LogInformation($"Mining genesis block"); } var difficulty = await _difficultyCalculator.CalculateDifficulty(prevHeader.Timestamp); var block = await AssembleBlock(prevHeader.BlockId, prevHeader.Height + 1, difficulty, _blockCancelToken.Token); @@ -88,7 +88,7 @@ private async void BuildTask() { if (block.Header.Status == BlockStatus.Confirmed) { - var recvResult = await _blockReciever.RecieveTail(block); + var recvResult = await _blockReciever.RecieveBlock(block); if (recvResult == PeerDataResult.Relay) _peerNetwork.BroadcastTail(block); } @@ -96,7 +96,7 @@ private async void BuildTask() } } - private void PendingTransactionList_Changed(object sender, EventArgs e) + private void UnconfirmedTransactionPoolChanged(object sender, EventArgs e) { _resetEvent.WaitOne(); try @@ -114,10 +114,9 @@ private void PendingTransactionList_Changed(object sender, EventArgs e) private async Task AssembleBlock(byte[] prevBlock, uint height, uint difficulty, CancellationToken cancellationToken) { - var targetTxns = _pendingTransactionList.Get; - targetTxns.Add(_blockbaseBuilder.Build(_builderKeys, targetTxns)); - var hashDict = HashTransactions(targetTxns, cancellationToken); - var merkleRoot = await _merkleTreeBuilder.BuildTree(hashDict.Keys); + var targetTxns = _unconfirmedTransactionPool.Get; + targetTxns.Add(await _blockbaseBuilder.Build(_builderKeys, targetTxns)); + var merkleRoot = await _merkleTreeBuilder.BuildTree(targetTxns.Select(x => x.TransactionId).ToList()); if (cancellationToken.IsCancellationRequested) { @@ -131,8 +130,7 @@ private async Task AssembleBlock(byte[] prevBlock, uint height, uint diff MerkleRootNode = merkleRoot, Header = new BlockHeader() { - MerkelRoot = merkleRoot.Value, - Timestamp = DateTime.UtcNow.Ticks, + MerkelRoot = merkleRoot.Value, Status = BlockStatus.Unconfirmed, Version = _networkParameters.HeaderVersion, Height = height, @@ -141,8 +139,8 @@ private async Task AssembleBlock(byte[] prevBlock, uint height, uint diff } }; - _logger.LogDebug($"Notarizing block {height}"); - await _blockNotary.ConfirmBlock(result, cancellationToken); + _logger.LogDebug($"Building consensus for block {height}"); + await _consensusMethod.BuildConsensus(result, cancellationToken); if (cancellationToken.IsCancellationRequested) { @@ -154,21 +152,5 @@ private async Task AssembleBlock(byte[] prevBlock, uint height, uint diff return result; } - - - - private IDictionary HashTransactions(ICollection transactions, CancellationToken cancellationToken) - { - var result = new ConcurrentDictionary(); - - Parallel.ForEach(transactions, txn => - { - var key = _transactionKeyResolver.ResolveKey(txn); - result[key] = txn; - }); - - return result; - } - } } diff --git a/NBlockchain/Services/BlockVerifier.cs b/NBlockchain/Services/BlockVerifier.cs index ed0b84d..e4b4625 100644 --- a/NBlockchain/Services/BlockVerifier.cs +++ b/NBlockchain/Services/BlockVerifier.cs @@ -12,32 +12,31 @@ namespace NBlockchain.Services public class BlockVerifier : IBlockVerifier { private readonly INetworkParameters _parameters; - private readonly IEnumerable _txnValidators; - private readonly IEnumerable _validTxnTypes; - private readonly IAddressEncoder _addressEncoder; + private readonly IEnumerable _txnRules; + private readonly IEnumerable _blockRules; + private readonly IInstructionRepository _instructionRepository; private readonly ISignatureService _signatureService; - private readonly IHashTester _hashTester; + private readonly IConsensusMethod _consensusMethod; private readonly IHasher _hasher; private readonly IMerkleTreeBuilder _merkleTreeBuilder; private readonly ITransactionKeyResolver _transactionKeyResolver; - private readonly IEqualityComparer _byteArrayEqualityComparer = new ByteArrayEqualityComparer(); - public BlockVerifier(INetworkParameters parameters, IAddressEncoder addressEncoder, ISignatureService signatureService, IEnumerable txnValidators, IEnumerable validTxnTypes, IMerkleTreeBuilder merkleTreeBuilder, ITransactionKeyResolver transactionKeyResolver, IHashTester hashTester, IHasher hasher) + public BlockVerifier(INetworkParameters parameters, ISignatureService signatureService, IEnumerable txnRules, IEnumerable blockRules, IEnumerable validTxnTypes, IMerkleTreeBuilder merkleTreeBuilder, ITransactionKeyResolver transactionKeyResolver, IConsensusMethod consensusMethod, IHasher hasher, IInstructionRepository instructionRepository) { _parameters = parameters; - _addressEncoder = addressEncoder; _signatureService = signatureService; - _txnValidators = txnValidators; - _validTxnTypes = validTxnTypes; + _txnRules = txnRules; + _blockRules = blockRules; _merkleTreeBuilder = merkleTreeBuilder; _transactionKeyResolver = transactionKeyResolver; - _hashTester = hashTester; + _consensusMethod = consensusMethod; _hasher = hasher; + _instructionRepository = instructionRepository; } - public bool Verify(Block block) + public async Task Verify(Block block) { - if (!_hashTester.TestHash(block.Header.BlockId, block.Header.Difficulty)) + if (!_consensusMethod.VerifyConsensus(block)) return false; var seed = block.Header.CombineHashableElementsWithNonce(block.Header.Nonce); @@ -45,63 +44,67 @@ public bool Verify(Block block) if (!hash.SequenceEqual(block.Header.BlockId)) return false; - - var hashDict = HashTransactions(block.Transactions); - var merkleRoot = _merkleTreeBuilder.BuildTree(hashDict.Keys).Result; + + var merkleRoot = await _merkleTreeBuilder.BuildTree(block.Transactions.Select(x => x.TransactionId).ToList()); if (!merkleRoot.Value.SequenceEqual(block.Header.MerkelRoot)) return false; + + return true; + } + public async Task VerifyTransactions(Block block) + { foreach (var txn in block.Transactions) { var siblings = block.Transactions.Where(x => x != txn).ToList(); - if (VerifyTransaction(txn, siblings) != 0) + if (await VerifyTransaction(txn, siblings) != 0) return false; } - return true; } - public bool VerifyContentThreshold(ICollection actual, ICollection expected) + public async Task VerifyBlockRules(Block block, bool tail) { - if (expected.Count == 0) - return true; - - var count = expected.Count(txn => actual.Contains(txn, _byteArrayEqualityComparer)); - var ratio = (decimal)count / (decimal)expected.Count; - return (ratio >= _parameters.ExpectedContentThreshold); + foreach (var rule in _blockRules.Where(x => x.TailRule == tail || tail)) + { + if (! await rule.Validate(block)) + return false; + } + return true; } - public int VerifyTransaction(TransactionEnvelope transaction, ICollection siblings) + public async Task VerifyTransaction(Transaction transaction, ICollection siblings) { - var result = 0; + foreach (var instruction in transaction.Instructions) + if (!await VerifyInstruction(instruction, siblings)) + return -1; - if (!_addressEncoder.IsValidAddress(transaction.Originator)) - return -1; + var expectedId = await _transactionKeyResolver.ResolveKey(transaction); - if (_validTxnTypes.All(x => x.TransactionType != transaction.TransactionType)) + if (!expectedId.SequenceEqual(transaction.TransactionId)) return -2; - if (!_signatureService.VerifyTransaction(transaction)) - return -3; - - foreach (var validator in _txnValidators.Where(v => v.TransactionType == transaction.TransactionType)) - result = result & validator.Validate(transaction, siblings); + foreach (var txnRule in _txnRules) + { + var txnResult = txnRule.Validate(transaction, siblings); + if (txnResult != 0) + return txnResult; + } - return result; + return 0; } - - private IDictionary HashTransactions(ICollection transactions) + + + private async Task VerifyInstruction(Instruction instruction, ICollection siblings) { - var result = new ConcurrentDictionary(); + if (siblings.Any(x => x.Instructions.Any(y => y.InstructionId.SequenceEqual(instruction.InstructionId)))) + return false; - Parallel.ForEach(transactions, txn => - { - var key = _transactionKeyResolver.ResolveKey(txn); - result[key] = txn; - }); + if (!_signatureService.VerifyInstruction(instruction)) + return false; - return result; + return (!await _instructionRepository.HaveInstruction(instruction.InstructionId)); } } } diff --git a/NBlockchain/Services/BlockbaseTransactionBuilder.cs b/NBlockchain/Services/BlockbaseTransactionBuilder.cs index 670770e..72668d5 100644 --- a/NBlockchain/Services/BlockbaseTransactionBuilder.cs +++ b/NBlockchain/Services/BlockbaseTransactionBuilder.cs @@ -2,42 +2,34 @@ using System.Collections.Generic; using System.Reflection; using System.Text; +using System.Threading.Tasks; using NBlockchain.Interfaces; using NBlockchain.Models; using Newtonsoft.Json.Linq; namespace NBlockchain.Services { - public abstract class BlockbaseTransactionBuilder : IBlockbaseTransactionBuilder + public abstract class BlockbaseTransactionBuilder : IBlockbaseTransactionBuilder { + protected readonly IAddressEncoder AddressEncoder; + protected readonly ISignatureService SignatureService; + protected readonly ITransactionBuilder TransactionBuilder; - private readonly IAddressEncoder _addressEncoder; - private readonly ISignatureService _signatureService; - private readonly TransactionTypeAttribute _transactionTypeMetadata; - - protected BlockbaseTransactionBuilder(IAddressEncoder addressEncoder, ISignatureService signatureService) + protected BlockbaseTransactionBuilder(IAddressEncoder addressEncoder, ISignatureService signatureService, ITransactionBuilder transactionBuilder) { - _addressEncoder = addressEncoder; - _signatureService = signatureService; - var typeInfo = typeof(T).GetTypeInfo(); - _transactionTypeMetadata = typeInfo.GetCustomAttribute(); + AddressEncoder = addressEncoder; + SignatureService = signatureService; + TransactionBuilder = transactionBuilder; } - public TransactionEnvelope Build(KeyPair builderKeys, ICollection transactions) + public async Task Build(KeyPair builderKeys, ICollection transactions) { - var result = new TransactionEnvelope(); - - result.Originator = _addressEncoder.EncodeAddress(builderKeys.PublicKey, 0); - result.OriginKey = Guid.NewGuid(); - result.TransactionType = _transactionTypeMetadata.TypeId; - result.Transaction = BuildBaseTransaction(transactions); - - _signatureService.SignTransaction(result, builderKeys.PrivateKey); - - return result; + var instructions = BuildInstructions(builderKeys, transactions); + return await TransactionBuilder.Build(instructions); } - protected abstract T BuildBaseTransaction(ICollection transactions); + protected abstract ICollection BuildInstructions(KeyPair builderKeys, ICollection transactions); + } } diff --git a/NBlockchain/Services/BlockchainNode.cs b/NBlockchain/Services/BlockchainNode.cs new file mode 100644 index 0000000..6dbad1d --- /dev/null +++ b/NBlockchain/Services/BlockchainNode.cs @@ -0,0 +1,279 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Concurrent; +using NBlockchain.Interfaces; +using NBlockchain.Models; + +namespace NBlockchain.Services +{ + public class BlockchainNode : IBlockchainNode + { + private readonly INetworkParameters _parameters; + private readonly IBlockRepository _blockRepository; + private readonly IBlockVerifier _blockVerifier; + private readonly ILogger _logger; + private readonly IForkRebaser _forkRebaser; + private readonly IReceiver _receiver; + private readonly IPeerNetwork _peerNetwork; + private readonly AutoResetEvent _blockEvent = new AutoResetEvent(true); + private readonly IUnconfirmedTransactionPool _unconfirmedTransactionPool; + private readonly IDifficultyCalculator _difficultyCalculator; + + public readonly Timer PollTimer; + + + public BlockchainNode(IBlockRepository blockRepository, IBlockVerifier blockVerifier, IReceiver receiver, ILoggerFactory loggerFactory, IForkRebaser forkRebaser, INetworkParameters parameters, IUnconfirmedTransactionPool unconfirmedTransactionPool, IPeerNetwork peerNetwork, IDifficultyCalculator difficultyCalculator) + { + _blockRepository = blockRepository; + _blockVerifier = blockVerifier; + _receiver = receiver; + _parameters = parameters; + _forkRebaser = forkRebaser; + _unconfirmedTransactionPool = unconfirmedTransactionPool; + _peerNetwork = peerNetwork; + _difficultyCalculator = difficultyCalculator; + //_expectedBlockList = expectedBlockList; + _logger = loggerFactory.CreateLogger(); + + _receiver.OnReceiveBlock += RecieveBlock; + _receiver.OnRecieveTransaction += RecieveTransaction; + + PollTimer = new Timer(GetMissingBlocks, null, TimeSpan.FromSeconds(5), _parameters.BlockTime); + } + + public async Task RecieveBlock(Block block) + { + var result = PeerDataResult.Ignore; + if (!_blockEvent.WaitOne(TimeSpan.FromSeconds(30))) + { + _logger.LogError($"Timeout waiting for block lock event"); + return PeerDataResult.Ignore; + } + try + { + result = await RecieveBlockUnprotected(block); + } + finally + { + _blockEvent.Set(); + } + + if (result != PeerDataResult.Demerit) + { + GetMissingBlocks(null); + } + + return result; + } + + private async Task RecieveBlockUnprotected(Block block) + { + var isTip = false; + + _logger.LogInformation($"Recv block {block.Header.Height} {BitConverter.ToString(block.Header.BlockId)}"); + + if (await _blockRepository.HavePrimaryBlock(block.Header.BlockId)) + { + _logger.LogInformation("already have block"); + return PeerDataResult.Ignore; + } + + if (!await _blockVerifier.Verify(block)) + { + _logger.LogWarning($"Block verification failed for {BitConverter.ToString(block.Header.BlockId)}"); + return PeerDataResult.Demerit; + } + + if (!await _blockVerifier.VerifyBlockRules(block, true)) + { + _logger.LogWarning($"Block rules failed for {BitConverter.ToString(block.Header.BlockId)}"); + return PeerDataResult.Demerit; + } + + var prevHeader = await _blockRepository.GetBlockHeader(block.Header.PreviousBlock); + var bestHeader = await _blockRepository.GetBestBlockHeader(); + var isEmpty = await _blockRepository.IsEmpty(); + bool mainChain = false; + bool rebaseChain = false; + isTip = (block.Header.PreviousBlock.SequenceEqual(bestHeader?.BlockId ?? Block.HeadKey)); + _logger.LogInformation($"Is Tip {isTip}"); + + if (prevHeader != null) + { + _logger.LogInformation("Do Have previous block"); + + if (block.Header.Timestamp < prevHeader.Timestamp) + { + _logger.LogInformation("Timestamps dont match"); + return PeerDataResult.Ignore; + } + + if (block.Header.Height != (prevHeader.Height + 1)) + { + _logger.LogInformation($"Height mismatch prev: {prevHeader.Height}, this: {block.Header.Height}"); + return PeerDataResult.Ignore; + } + + if (!isTip) + { + var prevMain = await _blockRepository.GetPrimaryHeader(block.Header.Height - 1); + var isPrevOnMainChain = prevMain?.BlockId.SequenceEqual(block.Header.PreviousBlock) ?? false; + var mainExisiting = await _blockRepository.GetPrimaryHeader(block.Header.Height); + mainChain = ((mainExisiting == null) && (isPrevOnMainChain)); + } + else + { + mainChain = true; + } + + rebaseChain = ((block.Header.Height > bestHeader.Height) && !mainChain); + + _logger.LogInformation($"Processing block, have prev, main chain: {mainChain}, rebase: {rebaseChain}"); + + var expectedDifficulty = await _difficultyCalculator.CalculateDifficulty(prevHeader.Timestamp); + if ((mainChain) && (block.Header.Difficulty < expectedDifficulty)) + { + _logger.LogInformation("Difficulty mismatch"); + return PeerDataResult.Ignore; + } + } + else + { + _logger.LogInformation("Dont have prev block"); + if (isEmpty && !block.Header.PreviousBlock.SequenceEqual(Block.HeadKey)) + { + _logger.LogInformation("not first block but am empty"); + return PeerDataResult.Ignore; + } + mainChain = isEmpty; + _logger.LogInformation($"Processing block, missing prev, main chain: {mainChain}, rebase: {rebaseChain}"); + _peerNetwork.RequestBlock(block.Header.PreviousBlock); + } + + if (mainChain) + { + _logger.LogInformation("processing for main chain"); + if (!await _blockVerifier.VerifyTransactions(block)) + { + _logger.LogWarning($"Block txn verification failed for {BitConverter.ToString(block.Header.BlockId)}"); + return PeerDataResult.Demerit; + } + + await _blockRepository.AddBlock(block); + _unconfirmedTransactionPool.Remove(block.Transactions); + } + else + { + if (!await _blockRepository.HaveSecondaryBlock(block.Header.BlockId)) + { + _logger.LogInformation($"Adding detached block"); + await _blockRepository.AddSecondaryBlock(block); + } + if (rebaseChain) + { + _logger.LogInformation($"Searching for divergent block"); + var divergentHeader = await _blockRepository.GetDivergentHeader(block.Header.BlockId); + if (divergentHeader != null) + { + _logger.LogInformation($"Rebasing chain from {divergentHeader.Height}"); + var replayBlocks = await _forkRebaser.RebaseChain(divergentHeader.BlockId, block.Header.BlockId); + _logger.LogInformation($"Replaying {replayBlocks.Count} blocks"); + foreach (var replayBlock in replayBlocks) + { + var replayResult = await RecieveBlockUnprotected(replayBlock); + if (replayResult == PeerDataResult.Demerit) + break; + } + _logger.LogInformation($"Replay complete"); + } + else + { + _logger.LogInformation($"Divergent block not found"); + var firstForkHeader = await _forkRebaser.FindKnownForkbase(block.Header.BlockId); + if (firstForkHeader != null) + _peerNetwork.RequestBlock(firstForkHeader.PreviousBlock); + } + } + } + + if (isTip) + { + _logger.LogDebug($"Accepted tip block {BitConverter.ToString(block.Header.BlockId)}"); + return PeerDataResult.Relay; + } + else + { + _logger.LogDebug($"Accepted block {BitConverter.ToString(block.Header.BlockId)}"); + return PeerDataResult.Ignore; + } + } + + public async Task RecieveTransaction(Transaction transaction) + { + _logger.LogDebug($"Recv txn {BitConverter.ToString(transaction.TransactionId)}"); + var txnResult = await _blockVerifier.VerifyTransaction(transaction, _unconfirmedTransactionPool.Get); + + if (txnResult == 0) + { + if (_unconfirmedTransactionPool.Add(transaction)) + { + _logger.LogDebug($"Accepted txn {BitConverter.ToString(transaction.TransactionId)}"); + return PeerDataResult.Relay; + } + + return PeerDataResult.Ignore; + } + _logger.LogDebug($"Rejected txn {BitConverter.ToString(transaction.TransactionId)} code: {txnResult}"); + return PeerDataResult.Ignore; + } + + + public async Task SendTransaction(Transaction transaction) + { + _logger.LogDebug("Sending txn"); + if (await _receiver.RecieveTransaction(transaction) == PeerDataResult.Relay) + _peerNetwork.BroadcastTransaction(transaction); + } + + private async void GetMissingBlocks(object state) + { + _logger.LogInformation("GetMissingBlocks"); + + var bestHeader = await _blockRepository.GetBestBlockHeader(); + + if (bestHeader == null) + { + _logger.LogInformation("Requesting head block"); + //_expectedBlockList.ExpectNext(Block.HeadKey); + _peerNetwork.RequestNextBlock(Block.HeadKey); + return; + } + + //if ((DateTime.UtcNow.Ticks - prevHeader.Timestamp) > _parameters.BlockTime.Ticks) + { + _logger.LogInformation($"Requesting missing block after {BitConverter.ToString(bestHeader.BlockId)}"); + //_expectedBlockList.ExpectNext(prevHeader.BlockId); + var cached = await _blockRepository.GetNextBlock(bestHeader.BlockId); + if (cached == null) + { + //_peerNetwork.RequestNextBlock(bestHeader.BlockId); + _peerNetwork.RequestBlockByHeight(bestHeader.Height + 1); + } + else + { + _logger.LogInformation("Have cached block"); + if (await _receiver.RecieveBlock(cached) == PeerDataResult.Demerit) + { + await _blockRepository.DiscardSecondaryBlock(cached.Header.BlockId); + } + } + } + } + + } +} diff --git a/NBlockchain/Services/Database/BlockStatistics.cs b/NBlockchain/Services/Database/BlockStatistics.cs new file mode 100644 index 0000000..6511c3a --- /dev/null +++ b/NBlockchain/Services/Database/BlockStatistics.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace NBlockchain.Services.Database +{ + public class BlockStatistics + { + public int BlockTime { get; set; } + + public DateTime TimeStamp { get; set; } + } +} diff --git a/NBlockchain/Services/Database/DefaultBlockRepository.cs b/NBlockchain/Services/Database/DefaultBlockRepository.cs index 4edca43..0f716a4 100644 --- a/NBlockchain/Services/Database/DefaultBlockRepository.cs +++ b/NBlockchain/Services/Database/DefaultBlockRepository.cs @@ -15,64 +15,235 @@ public class DefaultBlockRepository : IBlockRepository { private readonly ILogger _logger; private readonly IDataConnection _connection; + private readonly IAddressEncoder _addressEncoder; - protected LiteCollection> Blocks => _connection.Database.GetCollection>("Blocks"); + protected LiteCollection MainChain => _connection.Database.GetCollection("MainChain"); + protected LiteCollection ForkChain => _connection.Database.GetCollection("ForkChain"); + protected LiteCollection Instructions => _connection.Database.GetCollection("Instructions"); - public DefaultBlockRepository(ILoggerFactory loggerFactory, IDataConnection connection) + public DefaultBlockRepository(ILoggerFactory loggerFactory, IDataConnection connection, IAddressEncoder addressEncoder) { _connection = connection; + _addressEncoder = addressEncoder; _logger = loggerFactory.CreateLogger(); - Blocks.EnsureIndex(x => x.Entity.Header.BlockId); - Blocks.EnsureIndex(x => x.Entity.Header.PreviousBlock); - Blocks.EnsureIndex(x => x.Entity.Header.Height); + MainChain.EnsureIndex(x => x.Entity.Header.BlockId, true); + MainChain.EnsureIndex(x => x.Entity.Header.PreviousBlock, true); + MainChain.EnsureIndex(x => x.Entity.Header.Height, true); + + ForkChain.EnsureIndex(x => x.Entity.Header.BlockId); + ForkChain.EnsureIndex(x => x.Entity.Header.PreviousBlock); + ForkChain.EnsureIndex(x => x.Entity.Header.Height); + + Instructions.EnsureIndex(x => x.BlockId); + Instructions.EnsureIndex(x => x.TransactionId); + Instructions.EnsureIndex(x => x.Entity.InstructionId); + Instructions.EnsureIndex(x => x.Entity.PublicKey); + Instructions.EnsureIndex(x => x.Statistics.PublicKeyHash); } public Task AddBlock(Block block) { - Blocks.Insert(new PersistedEntity(block)); + var persisted = new PersistedBlock(block); + var prevHeader = MainChain + .Find(x => x.Entity.Header.BlockId == block.Header.PreviousBlock) + .Select(x => x.Entity.Header) + .FirstOrDefault(); + + if (prevHeader != null) + persisted.Statistics.BlockTime = Convert.ToInt32(TimeSpan.FromTicks(block.Header.Timestamp - prevHeader.Timestamp).TotalSeconds); + + MainChain.Insert(persisted); + + foreach (var txn in block.Transactions) + { + var pt = txn.Instructions.Select(ins => new PersistedInstruction(block.Header.BlockId, txn.TransactionId, ins, _addressEncoder.HashPublicKey(ins.PublicKey))).ToList(); + Instructions.InsertBulk(pt); + } + + ForkChain.Delete(x => x.Entity.Header.BlockId == block.Header.BlockId); + return Task.CompletedTask; } - public Task HaveBlock(byte[] blockId) + public Task HavePrimaryBlock(byte[] blockId) { - var result = Blocks.Exists(x => x.Entity.Header.BlockId.SequenceEqual(blockId)); + var result = MainChain.Exists(x => x.Entity.Header.BlockId == blockId); return Task.FromResult(result); } public Task IsEmpty() { - var count = Blocks.Count(); + var count = MainChain.Count(); return Task.FromResult(count == 0); } - public async Task GetNewestBlockHeader() + public async Task GetBestBlockHeader() { if (await IsEmpty()) return null; - var max = Blocks.Max(x => x.Entity.Header.Height).AsInt32; - var block = Blocks.FindOne(x => x.Entity.Header.Height == max); - return await Task.FromResult(block?.Entity.Header); + var max = MainChain.Max(x => x.Entity.Header.Height).AsInt64; + var block = MainChain.Find(Query.EQ("Entity.Header.Height", max)).FirstOrDefault(); + return block?.Entity.Header; } public Task GetNextBlock(byte[] prevBlockId) { - var block = Blocks.FindOne(x => x.Entity.Header.PreviousBlock.SequenceEqual(prevBlockId)); - return Task.FromResult(block?.Entity); + var persistedBlock = MainChain.FindOne(x => x.Entity.Header.PreviousBlock == prevBlockId); + + if (persistedBlock == null) + { + var forkBlock = ForkChain.FindOne(x => x.Entity.Header.PreviousBlock == prevBlockId); + return Task.FromResult(forkBlock?.Entity); + } + var result = RehydratePersistedBlock(persistedBlock); + + return Task.FromResult(result); } - public async Task GetGenesisBlockTime() + private Block RehydratePersistedBlock(PersistedBlock persistedBlock) { - if (await IsEmpty()) - return DateTime.UtcNow.Ticks; + var result = new Block(); + result.Header = persistedBlock.Entity.Header; + result.MerkleRootNode = persistedBlock.Entity.MerkleRootNode; + + var instructions = Instructions.Find(Query.EQ("BlockId", persistedBlock.Entity.Header.BlockId)); + result.Transactions = instructions + .GroupBy(x => x.TransactionId, new ByteArrayEqualityComparer()) + .Select(x => new Transaction(x.Select(y => y.Entity).ToList()) { TransactionId = x.Key }) + .ToList(); + + return result; + } + + public Task DiscardSecondaryBlock(byte[] blockId) + { + ForkChain.Delete(x => x.Entity.Header.BlockId == blockId); + return Task.CompletedTask; + } + + public Task GetAverageBlockTimeInSecs(DateTime startUtc, DateTime endUtc) + { + var startTicks = startUtc.Ticks; + var endTicks = endUtc.Ticks; + + var sample = MainChain.Find(Query.And(Query.LT("Entity.Header.Timestamp", endTicks), Query.GT("Entity.Header.Timestamp", startTicks))); + if (sample.Count() == 0) + return Task.FromResult(0); + + var result = Convert.ToInt32(sample.Average(x => x.Statistics.BlockTime)); + return Task.FromResult(result); + } + + public async Task GetBlockHeader(byte[] blockId) + { + var block = MainChain.Find(Query.EQ("Entity.Header.BlockId", blockId)).FirstOrDefault(); + if (block == null) + { + var fork = ForkChain.Find(Query.EQ("Entity.Header.BlockId", blockId)).FirstOrDefault(); + return fork?.Entity.Header; + } + + return block?.Entity.Header; + } + + public async Task GetBlock(byte[] blockId) + { + var block = MainChain.Find(Query.EQ("Entity.Header.BlockId", blockId)).FirstOrDefault(); + if (block == null) + { + var fork = ForkChain.Find(Query.EQ("Entity.Header.BlockId", blockId)).FirstOrDefault(); + return fork?.Entity; + } + + return RehydratePersistedBlock(block); + } + + public async Task GetPrimaryHeader(uint height) + { + //TODO: project result + var block = MainChain.Find(Query.EQ("Entity.Header.Height", Convert.ToInt64(height))).FirstOrDefault(); + return block?.Entity.Header; + } - return await Task.FromResult(Blocks.Min(x => x.Entity.Header.Timestamp)); + public async Task GetSecondaryHeader(byte[] forkBlockId) + { + //TODO: project result + var fork = ForkChain.Find(Query.EQ("Entity.Header.BlockId", forkBlockId)).FirstOrDefault(); + return fork?.Entity.Header; } - public Task GetAverageBlockTime(DateTime startUtc, DateTime endUtc) + public async Task AddSecondaryBlock(Block block) { - throw new NotImplementedException(); + ForkChain.Insert(new PersistedOrphan(block)); + } + + public async Task GetDivergentHeader(byte[] forkTipBlockId) + { + var forkHeader = await GetSecondaryHeader(forkTipBlockId); + if (forkHeader == null) + return null; + + while (!forkHeader.PreviousBlock.SequenceEqual(Block.HeadKey)) + { + var mainParent = MainChain.Find(x => x.Entity.Header.BlockId == forkHeader.PreviousBlock).FirstOrDefault(); + if (mainParent != null) + return mainParent.Entity.Header; + + forkHeader = await GetSecondaryHeader(forkHeader.PreviousBlock); + if (forkHeader == null) + return null; + } + return null; + } + + public async Task RewindChain(byte[] blockId) + { + var divergent = MainChain.Find(x => x.Entity.Header.BlockId == blockId).FirstOrDefault(); + if (divergent == null) + return; + + var archiveFork = MainChain + .Find(x => x.Entity.Header.Height > divergent.Entity.Header.Height) + .ToList() + .Select(RehydratePersistedBlock); + + foreach (var block in archiveFork.OrderByDescending(x => x.Header.Height)) + { + await AddSecondaryBlock(block); + Instructions.Delete(x => x.BlockId == block.Header.BlockId); + MainChain.Delete(x => x.Entity.Header.BlockId == block.Header.BlockId); + } + } + + public async Task> GetFork(byte[] forkTipBlockId) + { + var result = new List(); + + var forkBlock = ForkChain.Find(x => x.Entity.Header.BlockId == forkTipBlockId).FirstOrDefault(); + if (forkBlock == null) + return result; + + while (!forkBlock.Entity.Header.PreviousBlock.SequenceEqual(Block.HeadKey)) + { + result.Add(forkBlock.Entity); + var mainParent = MainChain.Find(x => x.Entity.Header.BlockId == forkBlock.Entity.Header.PreviousBlock).FirstOrDefault(); + if (mainParent != null) + break; + + forkBlock = ForkChain.Find(x => x.Entity.Header.BlockId == forkBlock.Entity.Header.PreviousBlock).FirstOrDefault(); + if (forkBlock == null) + break; + } + + return result.OrderBy(x => x.Header.Height).ToList(); + } + + public Task HaveSecondaryBlock(byte[] blockId) + { + var result = ForkChain.Exists(x => x.Entity.Header.BlockId == blockId); + return Task.FromResult(result); } } } \ No newline at end of file diff --git a/NBlockchain/Services/Database/DefaultPeerRepository.cs b/NBlockchain/Services/Database/DefaultPeerRepository.cs index 068cd99..f6d9ded 100644 --- a/NBlockchain/Services/Database/DefaultPeerRepository.cs +++ b/NBlockchain/Services/Database/DefaultPeerRepository.cs @@ -21,7 +21,7 @@ public DefaultPeerRepository(IDataConnection connection, ILoggerFactory loggerFa _logger = loggerFactory.CreateLogger(); } - protected LiteCollection> Peers => _connection.Database.GetCollection>("Peers"); + protected LiteCollection> Peers => _connection.Database.GetCollection>("Peers"); public async Task> DiscoverPeers() { @@ -42,7 +42,7 @@ public Task SharePeers(ICollection peers) } else { - Peers.Insert(new PersistedEntity(peer)); + Peers.Insert(new PersistedEntity(peer)); } } diff --git a/NBlockchain/Services/Database/InstructionRepository.cs b/NBlockchain/Services/Database/InstructionRepository.cs new file mode 100644 index 0000000..fa84cc4 --- /dev/null +++ b/NBlockchain/Services/Database/InstructionRepository.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using LiteDB; +using Microsoft.Extensions.Logging; +using NBlockchain.Interfaces; +using NBlockchain.Models; +using System.Linq; + +namespace NBlockchain.Services.Database +{ + public class InstructionRepository : IInstructionRepository + { + protected readonly ILogger Logger; + protected readonly IDataConnection Connection; + + protected LiteCollection MainChain => Connection.Database.GetCollection("MainChain"); + protected LiteCollection Instructions => Connection.Database.GetCollection("Instructions"); + + public InstructionRepository(ILoggerFactory loggerFactory, IDataConnection connection) + { + Connection = connection; + Logger = loggerFactory.CreateLogger(); + } + + public Task HaveInstruction(byte[] instructionId) + { + return Task.FromResult(Instructions.Exists(x => x.Entity.InstructionId == instructionId)); + } + } +} \ No newline at end of file diff --git a/NBlockchain/Services/Database/InstructionStatistics.cs b/NBlockchain/Services/Database/InstructionStatistics.cs new file mode 100644 index 0000000..84ab114 --- /dev/null +++ b/NBlockchain/Services/Database/InstructionStatistics.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace NBlockchain.Services.Database +{ + public class InstructionStatistics + { + public byte[] PublicKeyHash { get; set; } + } +} diff --git a/NBlockchain/Services/Database/PersistedBlock.cs b/NBlockchain/Services/Database/PersistedBlock.cs new file mode 100644 index 0000000..109ae11 --- /dev/null +++ b/NBlockchain/Services/Database/PersistedBlock.cs @@ -0,0 +1,52 @@ +using LiteDB; +using NBlockchain.Models; +using System; +using System.Collections.Generic; +using System.Text; + +namespace NBlockchain.Services.Database +{ + public class PersistedBlock : PersistedEntity + { + public PersistedBlock() + { + } + + public PersistedBlock(Block block) + { + Entity = new BlockInfo(block); + Statistics = new BlockStatistics(); + Statistics.TimeStamp = new DateTime(block.Header.Timestamp); + } + } + + public class PersistedOrphan : PersistedEntity + { + public PersistedOrphan() + { + } + + public PersistedOrphan(Block block) + { + Entity = block; + } + } + + + public class BlockInfo + { + public BlockHeader Header { get; set; } + public MerkleNode MerkleRootNode { get; set; } + + public BlockInfo() + { + } + + public BlockInfo(Block block) + { + Header = block.Header; + MerkleRootNode = block.MerkleRootNode; + } + } + +} diff --git a/NBlockchain/Services/Database/PersistedEntity.cs b/NBlockchain/Services/Database/PersistedEntity.cs new file mode 100644 index 0000000..1cd1f17 --- /dev/null +++ b/NBlockchain/Services/Database/PersistedEntity.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace NBlockchain.Services.Database +{ + public class PersistedEntity + where TStats : new() + { + public TKey Id { get; set; } + public TEntity Entity { get; set; } + public TStats Statistics { get; set; } + + public PersistedEntity() + { + Statistics = new TStats(); + } + + public PersistedEntity(TEntity entity) + { + Entity = entity; + Statistics = new TStats(); + } + } + + public class PersistedEntity + { + public TKey Id { get; set; } + public TEntity Entity { get; set; } + + public PersistedEntity() + { + } + + public PersistedEntity(TEntity entity) + { + Entity = entity; + } + } +} diff --git a/NBlockchain/Services/Database/PersistedInstruction.cs b/NBlockchain/Services/Database/PersistedInstruction.cs new file mode 100644 index 0000000..f74361c --- /dev/null +++ b/NBlockchain/Services/Database/PersistedInstruction.cs @@ -0,0 +1,27 @@ +using LiteDB; +using NBlockchain.Models; + +namespace NBlockchain.Services.Database +{ + public class PersistedInstruction : PersistedEntity + { + public byte[] BlockId { get; set; } + + public byte[] TransactionId { get; set; } + + public PersistedInstruction() + { + } + + public PersistedInstruction(byte[] blockId, byte[] transactionId, Instruction instruction, byte[] publicKeyHash) + { + Entity = instruction; + BlockId = blockId; + TransactionId = transactionId; + Statistics = new InstructionStatistics() + { + PublicKeyHash = publicKeyHash + }; + } + } +} \ No newline at end of file diff --git a/NBlockchain/Services/Database/TransactionRepository.cs b/NBlockchain/Services/Database/TransactionRepository.cs deleted file mode 100644 index 135afd6..0000000 --- a/NBlockchain/Services/Database/TransactionRepository.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading.Tasks; -using LiteDB; -using Microsoft.Extensions.Logging; -using NBlockchain.Interfaces; -using NBlockchain.Models; -using System.Linq; - -namespace NBlockchain.Services.Database -{ - public abstract class TransactionRepository - { - protected readonly ILogger Logger; - protected readonly IDataConnection Connection; - - protected LiteCollection> Blocks => Connection.Database.GetCollection>("Blocks"); - - protected TransactionRepository(ILoggerFactory loggerFactory, IDataConnection connection) - { - Connection = connection; - Logger = loggerFactory.CreateLogger(); - } - } -} \ No newline at end of file diff --git a/NBlockchain/Services/DefaultSignatureService.cs b/NBlockchain/Services/DefaultSignatureService.cs index c7ca735..b99bedd 100644 --- a/NBlockchain/Services/DefaultSignatureService.cs +++ b/NBlockchain/Services/DefaultSignatureService.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using System.Security.Cryptography; using System.Linq; +using System.Security.Cryptography; using System.Text; using NBlockchain.Interfaces; using NBlockchain.Models; @@ -12,88 +12,73 @@ namespace NBlockchain.Services public class DefaultSignatureService : ISignatureService { private readonly IAddressEncoder _addressEncoder; - private readonly ITransactionKeyResolver _transactionKeyResolver; - private readonly ECCurve _curve = ECCurve.NamedCurves.nistP256; - private readonly HashAlgorithmName _hashAlgorithm = HashAlgorithmName.SHA512; + private readonly IAsymetricCryptographyService _asymetricCryptography; - public DefaultSignatureService(IAddressEncoder addressEncoder, ITransactionKeyResolver transactionKeyResolver) + public DefaultSignatureService(IAddressEncoder addressEncoder, IAsymetricCryptographyService asymetricCryptography) { _addressEncoder = addressEncoder; - _transactionKeyResolver = transactionKeyResolver; + _asymetricCryptography = asymetricCryptography; } public KeyPair GenerateKeyPair() { - using (var dsa = ECDsa.Create(_curve)) + var privateKey = _asymetricCryptography.GeneratePrivateKey(); + var publicKey = _asymetricCryptography.GetPublicKey(privateKey); + + return new KeyPair() { - var parameters = dsa.ExportParameters(true); - var publicKey = parameters.Q.X.Concat(parameters.Q.Y).ToArray(); - return new KeyPair() - { - PrivateKey = parameters.D.Concat(publicKey).ToArray(), - PublicKey = publicKey - }; - } + PrivateKey = privateKey, + PublicKey = publicKey + }; } - public void SignTransaction(TransactionEnvelope transaction, byte[] privateKey) + public KeyPair GetKeyPairFromPhrase(string phrase) { - using (var dsa = ECDsa.Create(_curve)) - { - dsa.ImportParameters(new ECParameters() - { - Curve = _curve, - D = privateKey.Take(privateKey.Length / 3).ToArray(), - Q = new ECPoint() - { - X = privateKey.Skip(privateKey.Length / 3).Take(privateKey.Length / 3).ToArray(), - Y = privateKey.Skip((privateKey.Length / 3) * 2).Take(privateKey.Length / 3).ToArray() - } - }); + var privateKey = _asymetricCryptography.BuildPrivateKeyFromPhrase(phrase); + var publicKey = _asymetricCryptography.GetPublicKey(privateKey); - var data = ExtractSignableElements(transaction); + return new KeyPair() + { + PrivateKey = privateKey, + PublicKey = publicKey + }; + } - transaction.Signature = dsa.SignData(data, _hashAlgorithm); - } + public void SignInstruction(Instruction instruction, byte[] privateKey) + { + instruction.OriginKey = GenerateOriginKey(); + instruction.InstructionId = ResolveInstructionId(instruction); + instruction.Signature = _asymetricCryptography.Sign(instruction.InstructionId, privateKey); } - public bool VerifyTransaction(TransactionEnvelope transaction) + public bool VerifyInstruction(Instruction instruction) { - if (transaction.Signature == null) + if (instruction.Signature == null) return false; - if (!_addressEncoder.IsValidAddress(transaction.Originator)) + if (!instruction.InstructionId.SequenceEqual(ResolveInstructionId(instruction))) return false; - var pubKey = _addressEncoder.ExtractPublicKey(transaction.Originator); - - using (var dsa = ECDsa.Create(_curve)) + return _asymetricCryptography.Verify(instruction.InstructionId, instruction.Signature, instruction.PublicKey); + } + + private static byte[] ResolveInstructionId(Instruction instruction) + { + using (var h = SHA256.Create()) { - dsa.ImportParameters(new ECParameters() - { - Curve = _curve, - Q = new ECPoint() - { - X = pubKey.Take(pubKey.Length / 2).ToArray(), - Y = pubKey.Skip(pubKey.Length / 2).Take(pubKey.Length / 2).ToArray() - } - }); + var items = instruction.ExtractSignableElements(); + items.Add(instruction.OriginKey); + items.Add(instruction.PublicKey); - var data = ExtractSignableElements(transaction); + var data = items.SelectMany(x => x).ToArray(); - return dsa.VerifyData(data, transaction.Signature, _hashAlgorithm); + return h.ComputeHash(data); } } - private byte[] ExtractSignableElements(TransactionEnvelope txn) + private static byte[] GenerateOriginKey() { - var txnStr = JsonConvert.SerializeObject(txn.Transaction, Formatting.None); - - var result = txn.OriginKey.ToByteArray() - .Concat(Encoding.Unicode.GetBytes(txn.Originator)) - .Concat(Encoding.Unicode.GetBytes(txnStr)); - - return result.ToArray(); + return Guid.NewGuid().ToByteArray(); } } diff --git a/NBlockchain/Services/DifficultyCalculator.cs b/NBlockchain/Services/DifficultyCalculator.cs index bbfe101..bca27be 100644 --- a/NBlockchain/Services/DifficultyCalculator.cs +++ b/NBlockchain/Services/DifficultyCalculator.cs @@ -11,7 +11,8 @@ public class DifficultyCalculator : IDifficultyCalculator private readonly IBlockRepository _blockRepository; private readonly INetworkParameters _parameters; private readonly TimeSpan _sampleInterval = TimeSpan.FromHours(1); - private readonly uint _step = 50; + private readonly uint _step = 1; + private readonly uint _genesisValue = 700; public DifficultyCalculator(IBlockRepository blockRepository, INetworkParameters parameters) { @@ -23,13 +24,13 @@ public async Task CalculateDifficulty(long timestamp) { var end = new DateTime(timestamp + 1); var start = end.Subtract(_sampleInterval); - var latestHeader = await _blockRepository.GetNewestBlockHeader(); + var latestHeader = await _blockRepository.GetBestBlockHeader(); if (latestHeader == null) - return 0; + return _genesisValue; - var avg = await _blockRepository.GetAverageBlockTime(start, end); - var avgBlockTime = TimeSpan.FromTicks(avg); + var avg = await _blockRepository.GetAverageBlockTimeInSecs(start, end); + var avgBlockTime = TimeSpan.FromSeconds(avg); if (_parameters.BlockTime > avgBlockTime) return latestHeader.Difficulty + _step; diff --git a/NBlockchain/Services/ExpectedBlockList.cs b/NBlockchain/Services/ExpectedBlockList.cs new file mode 100644 index 0000000..e227950 --- /dev/null +++ b/NBlockchain/Services/ExpectedBlockList.cs @@ -0,0 +1,88 @@ +using NBlockchain.Interfaces; +using NBlockchain.Models; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace NBlockchain.Services +{ + public class ExpectedBlockList : IExpectedBlockList + { + private readonly Dictionary _expectedExpiries = new Dictionary(new ByteArrayEqualityComparer()); + private readonly AutoResetEvent _resetEvt = new AutoResetEvent(true); + private readonly INetworkParameters _parameters; + private readonly Timer _timer; + + public ExpectedBlockList(INetworkParameters parameters) + { + _parameters = parameters; + _timer = new Timer(ExpireUnconfirmed, null, _parameters.BlockTime, _parameters.BlockTime); + } + + public void ExpectNext(byte[] previousId) + { + _resetEvt.WaitOne(); + try + { + if (!_expectedExpiries.ContainsKey(previousId)) + _expectedExpiries[previousId] = DateTime.MaxValue; + } + finally + { + _resetEvt.Set(); + } + } + + public bool IsExpected(byte[] previousId) + { + _resetEvt.WaitOne(); + try + { + if (_expectedExpiries.ContainsKey(previousId)) + return (_expectedExpiries[previousId] > DateTime.Now); + } + finally + { + _resetEvt.Set(); + } + + return false; + } + + public void Confirm(byte[] previousId) + { + _resetEvt.WaitOne(); + try + { + if (_expectedExpiries.ContainsKey(previousId)) + _expectedExpiries[previousId] = DateTime.Now.Add(_parameters.BlockTime); + } + finally + { + _resetEvt.Set(); + } + } + + private void ExpireUnconfirmed(object state) + { + _resetEvt.WaitOne(); + try + { + var keys = new List(_expectedExpiries.Keys); + foreach (var key in keys) + { + if (_expectedExpiries[key] < DateTime.Now) + _expectedExpiries.Remove(key); + } + } + finally + { + _resetEvt.Set(); + } + } + + } +} diff --git a/NBlockchain/Services/ForkRebaser.cs b/NBlockchain/Services/ForkRebaser.cs new file mode 100644 index 0000000..39325b5 --- /dev/null +++ b/NBlockchain/Services/ForkRebaser.cs @@ -0,0 +1,49 @@ +using Microsoft.Extensions.Logging; +using NBlockchain.Interfaces; +using NBlockchain.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NBlockchain.Services +{ + public class ForkRebaser : IForkRebaser + { + private readonly IBlockRepository _blockRepository; + //private IReceiver _blockReceiver; + private readonly ILogger _logger; + + public ForkRebaser(IBlockRepository blockRepository, ILoggerFactory loggerFactory) + { + _blockRepository = blockRepository; + _logger = loggerFactory.CreateLogger(); + } + + public async Task> RebaseChain(byte[] divergentId, byte[] targetTipId) + { + _logger.LogInformation($"Rebasing chain from {BitConverter.ToString(divergentId)} to {BitConverter.ToString(targetTipId)}"); + var currentTipHeader = await _blockRepository.GetBestBlockHeader(); + await _blockRepository.RewindChain(divergentId); + var chainFork = await _blockRepository.GetFork(targetTipId); + return chainFork.OrderBy(x => x.Header.Height).ToList(); + } + + + public async Task FindKnownForkbase(byte[] forkTipId) + { + _logger.LogInformation($"Searching for fork base"); + var header = await _blockRepository.GetSecondaryHeader(forkTipId); + + var prevHeader = await _blockRepository.GetSecondaryHeader(header.PreviousBlock); + while (prevHeader != null) + { + header = prevHeader; + prevHeader = await _blockRepository.GetSecondaryHeader(prevHeader.PreviousBlock); + } + return header; + } + + } +} diff --git a/NBlockchain/Services/InMemoryBlockRepository.cs b/NBlockchain/Services/InMemoryBlockRepository.cs deleted file mode 100644 index 9c9ef17..0000000 --- a/NBlockchain/Services/InMemoryBlockRepository.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using NBlockchain.Interfaces; -using NBlockchain.Models; - -namespace NBlockchain.Services -{ - /// - /// In-memory block repository for testing & demo purposes - /// - public class InMemoryBlockRepository : IBlockRepository - { - private readonly AutoResetEvent _resetEvent = new AutoResetEvent(true); - private readonly ICollection _blocks = new HashSet(); - - public InMemoryBlockRepository() - { - } - - public async Task AddBlock(Block block) - { - _resetEvent.WaitOne(); - try - { - _blocks.Add(block); - await Task.Yield(); - } - finally - { - _resetEvent.Set(); - } - } - - public async Task HaveBlock(byte[] blockId) - { - _resetEvent.WaitOne(); - try - { - return await Task.FromResult(_blocks.Any(x => x.Header.BlockId.SequenceEqual(blockId))); - - } - finally - { - _resetEvent.Set(); - } - } - - public async Task GetNewestBlockHeader() - { - _resetEvent.WaitOne(); - try - { - if (!_blocks.Any()) - return null; - - var max = _blocks.Max(x => x.Header.Height); - var block = _blocks.FirstOrDefault(x => x.Header.Height == max); - return await Task.FromResult(block?.Header); - } - finally - { - _resetEvent.Set(); - } - } - - public async Task GetNextBlock(byte[] prevBlockId) - { - _resetEvent.WaitOne(); - try - { - var block = _blocks.FirstOrDefault(x => x.Header.PreviousBlock.SequenceEqual(prevBlockId)); - return await Task.FromResult(block); - } - finally - { - _resetEvent.Set(); - } - } - - public async Task IsEmpty() - { - _resetEvent.WaitOne(); - try - { - return await Task.FromResult(!_blocks.Any()); - } - finally - { - _resetEvent.Set(); - } - } - - public async Task GetAverageBlockTime(DateTime startUtc, DateTime endUtc) - { - _resetEvent.WaitOne(); - try - { - var startTicks = startUtc.Ticks; - var endTicks = endUtc.Ticks; - var avg = _blocks.Where(x => x.Header.Timestamp > startTicks && x.Header.Timestamp < endTicks && x.Header.Height > 1) - .Average(x => (x.Header.Timestamp - (_blocks.First(y => y.Header.BlockId.SequenceEqual(x.Header.PreviousBlock)).Header.Timestamp))); - - return Convert.ToInt64(avg); - } - finally - { - _resetEvent.Set(); - } - } - } -} diff --git a/NBlockchain/Services/NatTraversal/NoTraversal.cs b/NBlockchain/Services/NatTraversal/NoTraversal.cs new file mode 100644 index 0000000..9ab890f --- /dev/null +++ b/NBlockchain/Services/NatTraversal/NoTraversal.cs @@ -0,0 +1,13 @@ +using System.Net; +using NBlockchain.Interfaces; + +namespace NBlockchain.Services.NatTraversal +{ + public class NoTraversal : INatTraversal + { + public string ConfigureNatTraversal(IPAddress ownAddress, int internalPort) + { + return $"{ownAddress}:{internalPort}"; + } + } +} \ No newline at end of file diff --git a/NBlockchain/Services/NatTraversal/StaticPortForwarding.cs b/NBlockchain/Services/NatTraversal/StaticPortForwarding.cs new file mode 100644 index 0000000..e114918 --- /dev/null +++ b/NBlockchain/Services/NatTraversal/StaticPortForwarding.cs @@ -0,0 +1,22 @@ +using System.Net; +using NBlockchain.Interfaces; + +namespace NBlockchain.Services.NatTraversal +{ + public class StaticPortForwarding : INatTraversal + { + private readonly int _staticExternalPort; + private readonly IProvideUpnpDevice _upnpDeviceProvider; + + public StaticPortForwarding(int staticExternalPort, IProvideUpnpDevice upnpDeviceProvider) + { + _staticExternalPort = staticExternalPort; + _upnpDeviceProvider = upnpDeviceProvider; + } + public string ConfigureNatTraversal(IPAddress ownAddress, int internalPort) + { + var ip = _upnpDeviceProvider.GetExternalIp(); + return $"{ip}:{_staticExternalPort}"; + } + } +} \ No newline at end of file diff --git a/NBlockchain/Services/NatTraversal/UpnpAutodetectPortForwarding.cs b/NBlockchain/Services/NatTraversal/UpnpAutodetectPortForwarding.cs new file mode 100644 index 0000000..f77161b --- /dev/null +++ b/NBlockchain/Services/NatTraversal/UpnpAutodetectPortForwarding.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq; +using System.Net; +using Microsoft.Extensions.Logging; +using NBlockchain.Interfaces; + +namespace NBlockchain.Services.NatTraversal +{ + public class UpnpAutodetectPortForwarding : INatTraversal + { + private readonly string _description; + private readonly IProvideUpnpDevice _upnpDeviceProvider; + private readonly ILogger _logger; + + public UpnpAutodetectPortForwarding(string description, ILoggerFactory loggerFactory, IProvideUpnpDevice upnpDeviceProvider) + { + _description = description; + _upnpDeviceProvider = upnpDeviceProvider; + _logger = loggerFactory.CreateLogger(); + } + public string ConfigureNatTraversal(IPAddress ownAddress, int internalPort) + { + var ip = _upnpDeviceProvider.GetExternalIp(); + var allMappings = _upnpDeviceProvider.GetAllMappings(); + + var existingMapping = allMappings.SingleOrDefault(m => m.PrivatePort == internalPort && m.Description == _description); + if (existingMapping?.PrivateIP?.Equals(ownAddress) ?? false) + { + return $"{existingMapping.PublicIP}:{existingMapping.PublicPort}"; + } + if (!existingMapping?.PrivateIP?.Equals(ownAddress) ?? false) + { + _logger.LogError($"The port {internalPort} is in use by another IP: {existingMapping.PrivateIP}"); + throw new Exception($"The port {internalPort} is in use by another IP: {existingMapping.PrivateIP}"); + } + for (var nextPort = 49151; nextPort < 65535; nextPort++) + { + if (allMappings.Any(m => m.PublicPort == nextPort && !m.PrivateIP.Equals(ownAddress))) continue; + _upnpDeviceProvider.CreateMapping(internalPort, nextPort, _description); + return $"{ip}:{nextPort}"; + } + _logger.LogError("No available ports found."); + throw new Exception("No available ports found."); + } + } +} \ No newline at end of file diff --git a/NBlockchain/Services/NatTraversal/UpnpStaticPortForwarding.cs b/NBlockchain/Services/NatTraversal/UpnpStaticPortForwarding.cs new file mode 100644 index 0000000..48c4ce6 --- /dev/null +++ b/NBlockchain/Services/NatTraversal/UpnpStaticPortForwarding.cs @@ -0,0 +1,42 @@ +using System; +using System.Linq; +using System.Net; +using Microsoft.Extensions.Logging; +using NBlockchain.Interfaces; + +namespace NBlockchain.Services.NatTraversal +{ + public class UpnpStaticPortForwarding : INatTraversal + { + private readonly string _description; + private readonly int _externalPort; + private readonly IProvideUpnpDevice _upnpDeviceProvider; + private readonly ILogger _logger; + + public UpnpStaticPortForwarding(string description, int externalPort, ILoggerFactory loggerFactory, IProvideUpnpDevice upnpDeviceProvider) + { + _description = description; + _externalPort = externalPort; + _upnpDeviceProvider = upnpDeviceProvider; + _logger = loggerFactory.CreateLogger(); + } + public string ConfigureNatTraversal(IPAddress ownAddress, int internalPort) + { + var ip = _upnpDeviceProvider.GetExternalIp(); + var allMappings = _upnpDeviceProvider.GetAllMappings(); + + var existingMapping = allMappings.SingleOrDefault(m =>m.PrivatePort == internalPort && m.Description == _description); + if (existingMapping?.PublicIP?.Equals(ownAddress) ?? false) + { + return $"{existingMapping.PublicIP}:{existingMapping.PublicPort}"; + } + if (!existingMapping?.PrivateIP?.Equals(ownAddress) ?? false) + { + _logger.LogError($"The port {internalPort} is in use by another IP: {existingMapping.PrivateIP}"); + throw new Exception($"The port {internalPort} is in use by another IP: {existingMapping.PrivateIP}"); + } + _upnpDeviceProvider.CreateMapping(internalPort, _externalPort, _description); + return $"{ip}:{_externalPort}"; + } + } +} \ No newline at end of file diff --git a/NBlockchain/Services/Net/Handshake.cs b/NBlockchain/Services/Net/Handshake.cs new file mode 100644 index 0000000..72907dc --- /dev/null +++ b/NBlockchain/Services/Net/Handshake.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace NBlockchain.Services.Net +{ + public class Handshake + { + public Guid NodeId { get; set; } + public int Version { get; set; } + public long Height { get; set; } + + } +} diff --git a/NBlockchain/Services/Net/InProcessPeerNetwork.cs b/NBlockchain/Services/Net/InProcessPeerNetwork.cs index 7a9ed71..3276382 100644 --- a/NBlockchain/Services/Net/InProcessPeerNetwork.cs +++ b/NBlockchain/Services/Net/InProcessPeerNetwork.cs @@ -12,31 +12,28 @@ public class InProcessPeerNetwork : IPeerNetwork, IDisposable private static readonly IList Peers = new List(); private readonly IBlockRepository _blockRepository; - private IBlockReceiver _blockReciever; - private ITransactionReceiver _transactionReciever; + private IReceiver _reciever; public Guid NodeId { get; private set; } - public InProcessPeerNetwork(IBlockRepository blockRepository) + public InProcessPeerNetwork(IBlockRepository blockRepository, IReceiver reciever) { _blockRepository = blockRepository; + _reciever = reciever; NodeId = Guid.NewGuid(); Peers.Add(this); } + - public void RegisterBlockReceiver(IBlockReceiver blockReceiver) +#pragma warning disable CS1998 + public void RequestBlockByHeight(uint height) { - _blockReciever = blockReceiver; + throw new NotImplementedException(); } - - public void RegisterTransactionReceiver(ITransactionReceiver transactionReciever) - { - _transactionReciever = transactionReciever; - } - - public void DiscoverPeers() + public async Task DiscoverPeers() { } +#pragma warning restore CS1998 public void Open() { @@ -48,18 +45,18 @@ public void Close() public Action ReceiveBlock => (peer, block) => { - _blockReciever.RecieveBlock(block); + _reciever.RecieveBlock(block); }; public Action ReceiveTail => (peer, block) => { - _blockReciever.RecieveTail(block); + _reciever.RecieveBlock(block); }; - public Action ReceiveTransaction => (peer, txn) => + public Action ReceiveTransaction => (peer, txn) => { - _transactionReciever.RecieveTransaction(txn); + _reciever.RecieveTransaction(txn); }; public Action ReceiveBlockRequest => async (peer, txn) => @@ -80,7 +77,7 @@ public void BroadcastTail(Block block) }); } - public void BroadcastTransaction(TransactionEnvelope transaction) + public void BroadcastTransaction(Transaction transaction) { Parallel.ForEach(Peers.Where(x => x.NodeId != NodeId), peer => { @@ -100,5 +97,20 @@ public void Dispose() { Peers.Remove(this); } + + public ICollection GetPeersIn() + { + throw new NotImplementedException(); + } + + public ICollection GetPeersOut() + { + throw new NotImplementedException(); + } + + public void RequestBlock(byte[] blockId) + { + throw new NotImplementedException(); + } } } diff --git a/NBlockchain/Services/Net/PeerConnection.cs b/NBlockchain/Services/Net/PeerConnection.cs new file mode 100644 index 0000000..5c0bbd8 --- /dev/null +++ b/NBlockchain/Services/Net/PeerConnection.cs @@ -0,0 +1,337 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Bson; + +namespace NBlockchain.Services.Net +{ + public class PeerConnection + { + private const int MaxMessageSize = 10240000; + private const byte NetworkQualifier = 0; + private const byte MessageQualifier = 1; + + private const byte IdentifyCommand = 0; + private const byte PingCommand = 1; + + private readonly byte[] _serviceIdentifier; + private readonly TcpClient _client; + private readonly AutoResetEvent _resetEvent = new AutoResetEvent(true); + private readonly Guid _localId; + private readonly int _localVersion; + private int _remoteVersion; + + + private Guid _remoteId = Guid.NewGuid(); + private DateTime? _lastContact; + private CancellationTokenSource _cancelToken = new CancellationTokenSource(); + private bool _pollExited = false; + + public event ReceiveMessage OnReceiveMessage; + public event PeerEvent OnDisconnect; + public event PeerEvent OnIdentify; + public event PeerEvent OnUnresponsive; + public event PeerException OnPeerException; + + public Guid RemoteId => _remoteId; + + public EndPoint RemoteEndPoint => _client?.Client?.RemoteEndPoint; + public bool Outgoing { get; private set; } + public string ConnectionString { get; private set; } + public DateTime? LastContact => _lastContact; + + public long RequestCount { get; set; } = 0; + public int DemeritPoints { get; set; } = 0; + public TimeSpan QuietTimeout { get; set; } = TimeSpan.FromMinutes(10); + + public PeerConnection(string serviceIdentifier, int version, Guid nodeId) + { + _client = new TcpClient(); + _localId = nodeId; + _localVersion = version; + _serviceIdentifier = Encoding.UTF8.GetBytes(serviceIdentifier); + Outgoing = true; + } + + public PeerConnection(string serviceIdentifier, int version, Guid nodeId, TcpClient client) + { + _client = client; + _localId = nodeId; + _localVersion = version; + _serviceIdentifier = Encoding.UTF8.GetBytes(serviceIdentifier); + Outgoing = false; + } + + public async Task Connect(string connectionString) + { + ConnectionString = connectionString; + var uri = new Uri(connectionString); + if (uri.Scheme != "tcp") + throw new InvalidOperationException("Only tcp connections are possible"); + + await _client.ConnectAsync(uri.Host, uri.Port); + SendIdentify(); + } + + public void Run() + { + Task.Factory.StartNew(Poll); + } + + public void Send(byte command, byte[] data) + { + Send(MessageQualifier, command, data); + } + + public void Send(byte qualifier, byte command, byte[] data) + { + _resetEvent.WaitOne(); + try + { + //_client.Client.SendTimeout = 1000; + var headerLength = _serviceIdentifier.Length + 6; + var lenBuffer = BitConverter.GetBytes(data.Length); + var message = new byte[headerLength + data.Length]; + _serviceIdentifier.CopyTo(message, 0); + lenBuffer.CopyTo(message, _serviceIdentifier.Length); + message[_serviceIdentifier.Length + 4] = qualifier; + message[_serviceIdentifier.Length + 5] = command; + data.CopyTo(message, headerLength); + _client.Client.Send(message); + } + catch (SocketException ex) + { + switch (ex.SocketErrorCode) + { + case SocketError.ConnectionAborted: + case SocketError.ConnectionReset: + case SocketError.Disconnecting: + case SocketError.HostDown: + case SocketError.NetworkDown: + case SocketError.NotConnected: + case SocketError.Shutdown: + _cancelToken.Cancel(); + OnDisconnect?.Invoke(this); + Disconnect(); + break; + + } + } + catch (Exception ex) + { + OnPeerException?.Invoke(this, ex); + } + finally + { + _resetEvent.Set(); + } + } + + public void Disconnect() + { + _cancelToken.Cancel(); + _resetEvent.WaitOne(); + try + { + _client?.Client?.Shutdown(SocketShutdown.Both); + } + catch (Exception ex) + { + OnPeerException?.Invoke(this, ex); + } + finally + { + _resetEvent.Set(); + } + } + + public void Close() + { + Task.Factory.StartNew(() => + { + try + { + _cancelToken.Cancel(); + SpinWait.SpinUntil(() => _pollExited); + //_client?.GetStream().Dispose(); + _client?.Dispose(); + } + catch (Exception ex) + { + OnPeerException?.Invoke(this, ex); + } + }); + } + + private void Maintain(object state) + { + if (_client.Connected) + { + if (_lastContact < (DateTime.Now.Subtract(QuietTimeout))) + { + OnUnresponsive?.Invoke(this); + Disconnect(); + } + else + { + Send(NetworkQualifier, PingCommand, new byte[0]); + } + } + } + + private async void Poll() + { + _pollExited = false; + _cancelToken = new CancellationTokenSource(); + var headerLength = _serviceIdentifier.Length + 6; + var timer = new Timer(Maintain, null, TimeSpan.FromSeconds(120), TimeSpan.FromSeconds(120)); + SendIdentify(); + Send(NetworkQualifier, PingCommand, new byte[0]); + while ((_client.Connected) && (!_cancelToken.IsCancellationRequested)) + { + try + { + var header = new byte[headerLength]; + + if (Recieve(header) != headerLength) + continue; + + var servIdSegment = new ArraySegment(header, 0, _serviceIdentifier.Length).ToArray(); + var lengthSegment = new ArraySegment(header, _serviceIdentifier.Length, 4).ToArray(); + var commandSegment = new ArraySegment(header, _serviceIdentifier.Length + 4, 2).ToArray(); + + if (!servIdSegment.SequenceEqual(_serviceIdentifier)) + { + var flushBuffer = new byte[_client.Available]; + _client.Client.Receive(flushBuffer, _client.Available, SocketFlags.None); + continue; + } + + var msgLength = BitConverter.ToInt32(lengthSegment, 0); + + if (msgLength < 0) + continue; + + if (msgLength > MaxMessageSize) + continue; + + var msgBuffer = new byte[msgLength]; + + var actualRecv = Recieve(msgBuffer); + + if (actualRecv != msgLength) + continue; + + _lastContact = DateTime.Now; + + switch (commandSegment[0]) + { + case NetworkQualifier: + ProcessNetworkCommand(commandSegment[1], msgBuffer); + break; + default: + var evtTask = Task.Factory.StartNew(() => OnReceiveMessage?.Invoke(this, commandSegment[1], msgBuffer)); + break; + } + } + catch (SocketException ex) + { + switch (ex.SocketErrorCode) + { + case SocketError.ConnectionAborted: + case SocketError.ConnectionReset: + case SocketError.Disconnecting: + case SocketError.HostDown: + case SocketError.NetworkDown: + case SocketError.NotConnected: + case SocketError.Shutdown: + _cancelToken.Cancel(); + OnDisconnect?.Invoke(this); + Disconnect(); + break; + } + } + catch (Exception ex) + { + OnPeerException?.Invoke(this, ex); + await Task.Delay(1000); + } + } + timer.Dispose(); + _pollExited = true; + } + + private void SendIdentify() + { + var data = new Handshake() + { + NodeId = _localId, + Version = _localVersion + }; + Send(NetworkQualifier, IdentifyCommand, SerializeObject(data)); + } + + private void ProcessNetworkCommand(byte command, byte[] data) + { + switch (command) + { + case IdentifyCommand: + var handshake = DeserializeObject(data); + _remoteId = handshake.NodeId; + _remoteVersion = handshake.Version; + OnIdentify?.Invoke(this); + break; + case PingCommand: + break; + } + } + + private int Recieve(byte[] msgBuffer) + { + var actualRecv = 0; + while (actualRecv < msgBuffer.Length) + actualRecv += _client.Client.Receive(msgBuffer, actualRecv, (msgBuffer.Length - actualRecv), SocketFlags.None); + + return actualRecv; + } + + private static byte[] SerializeObject(object data) + { + using (var bw = new MemoryStream()) + { + var writer = new BsonDataWriter(bw); + var serializer = new JsonSerializer(); + serializer.TypeNameHandling = TypeNameHandling.Objects; + serializer.Serialize(writer, data); + writer.Close(); + bw.TryGetBuffer(out var result); + return result.Array; + } + } + + private static T DeserializeObject(byte[] bson) + { + using (var ms = new MemoryStream(bson)) + { + var bdr = new BsonDataReader(ms); + var serializer = new JsonSerializer(); + serializer.TypeNameHandling = TypeNameHandling.Objects; + var result = serializer.Deserialize(bdr); + bdr.Close(); + return result; + } + } + } + + public delegate void ReceiveMessage(PeerConnection sender, byte command, byte[] data); + public delegate void PeerEvent(PeerConnection sender); + public delegate void PeerException(PeerConnection sender, Exception exception); + +} diff --git a/NBlockchain/Services/Net/TcpPeerNetwork.cs b/NBlockchain/Services/Net/TcpPeerNetwork.cs index d002845..3f22a90 100644 --- a/NBlockchain/Services/Net/TcpPeerNetwork.cs +++ b/NBlockchain/Services/Net/TcpPeerNetwork.cs @@ -5,15 +5,15 @@ using System.Linq; using System.Net; using System.Net.Sockets; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using NBlockchain.Interfaces; using NBlockchain.Models; -using NetMQ; -using NetMQ.Sockets; using Newtonsoft.Json; using Newtonsoft.Json.Bson; +using System.Diagnostics; namespace NBlockchain.Services.Net { @@ -22,306 +22,397 @@ public class TcpPeerNetwork : IPeerNetwork, IDisposable //TODO: break this class up into smaller pieces private const int TargetOutgoingCount = 8; private readonly uint _port; + private readonly INatTraversal _natTraversal; + private string _serviceId = "BC"; + private int _version = 1; - private IBlockReceiver _blockReciever; - private ITransactionReceiver _transactionReciever; + private readonly IReceiver _reciever; private readonly IBlockRepository _blockRepository; private readonly IEnumerable _discoveryServices; private readonly ILogger _logger; private readonly IOwnAddressResolver _ownAddressResolver; + private readonly IUnconfirmedTransactionPool _unconfirmedTransactionPool; private readonly ConcurrentQueue _peerRoundRobin = new ConcurrentQueue(); - private readonly ConcurrentDictionary _outgoingConnectionStrings = new ConcurrentDictionary(); - private readonly ConcurrentDictionary _outgoingSockets = new ConcurrentDictionary(); - private readonly ConcurrentDictionary _incomingPeerLastContact = new ConcurrentDictionary(); + + private readonly List _peerConnections = new List(); + private CancellationTokenSource _cancelTokenSource; + private TcpListener _listener; + + private readonly AutoResetEvent _peerEvent = new AutoResetEvent(true); + private readonly AutoResetEvent _connectOutEvent = new AutoResetEvent(true); private Timer _sharePeersTimer; + private Timer _discoveryTimer; - private readonly RouterSocket _incomingSocket = new RouterSocket(); - private readonly NetMQPoller _poller = new NetMQPoller(); - private NetMQTimer _houseKeeper; - private string _internalConnsctionString; - private string _externalConnsctionString; + private string _internalConnectionString; + private string _externalConnectionString; + + private object _duplicateLock = new object(); public Guid NodeId { get; private set; } - public TcpPeerNetwork(uint port, IBlockRepository blockRepository, IEnumerable discoveryServices, ILoggerFactory loggerFactory, IOwnAddressResolver ownAddressResolver) + public TcpPeerNetwork(uint port, INatTraversal natTraversal, IBlockRepository blockRepository, IEnumerable discoveryServices, ILoggerFactory loggerFactory, IOwnAddressResolver ownAddressResolver, IUnconfirmedTransactionPool unconfirmedTransactionPool, IReceiver reciever) { _port = port; + _natTraversal = natTraversal; + _reciever = reciever; _logger = loggerFactory.CreateLogger(); _blockRepository = blockRepository; _discoveryServices = discoveryServices; _ownAddressResolver = ownAddressResolver; + _unconfirmedTransactionPool = unconfirmedTransactionPool; NodeId = Guid.NewGuid(); - _sharePeersTimer = new Timer(SharePeers, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); - _incomingSocket.ReceiveReady += IncomingSocketReceiveReady; } - private async void IncomingSocketReceiveReady(object sender, NetMQSocketEventArgs e) + private ICollection GetActivePeers() { + _peerEvent.WaitOne(); try { - var message = e.Socket.ReceiveMultipartMessage(); + return new List(_peerConnections); + } + finally + { + _peerEvent.Set(); + } + } + + public void Open() + { + _cancelTokenSource = new CancellationTokenSource(); + _listener = new TcpListener(IPAddress.Any, (int)_port); + _listener.Start(); - if (message.FrameCount < 2) + Task.Factory.StartNew(async () => + { + try { - return; + while (!_cancelTokenSource.IsCancellationRequested) + { + var client = await _listener.AcceptTcpClientAsync(); + _logger.LogDebug($"Client connected - {client.Client.RemoteEndPoint}"); + var peer = new PeerConnection(_serviceId, _version, NodeId, client); + AttachEventHandlers(peer); + _peerEvent.WaitOne(); + try + { + _peerConnections.Add(peer); + peer.Run(); + } + finally + { + _peerEvent.Set(); + } + } + } + catch (Exception ex) + { + _logger.LogError($"Error listening - {ex.Message}"); } + }); - var clientId = new Guid(message[0].Buffer); - var op = (MessageOp) (message[1].Buffer.First()); - _incomingPeerLastContact[clientId] = DateTime.Now; + DiscoverOwnConnectionStrings(); + AdvertiseToPeers(); - switch (op) + _discoveryTimer = new Timer(async (state) => + { + await DiscoverPeers(); + }, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(30)); + + _sharePeersTimer = new Timer(SharePeers, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); + + + Task.Factory.StartNew(async () => + { + while (!_cancelTokenSource.IsCancellationRequested) { - case MessageOp.Block: - await ProcessBlock(message, clientId, false); - break; - case MessageOp.Tail: - await ProcessBlock(message, clientId, true); - break; - case MessageOp.Txn: - await ProcessTransaction(message, clientId); - break; - case MessageOp.BlockRequest: - await ProcessBlockRequest(message, e.Socket, clientId); - break; - case MessageOp.PeerShare: - if (IsSharablePeer(message[2].ConvertToString())) - AddPeer(new KnownPeer() { ConnectionString = message[2].ConvertToString() }); - break; - case MessageOp.Connect: - _logger.LogDebug("Recv connect from {0}", clientId); - _incomingSocket.SendMoreFrame(message[0].Buffer) - .SendMoreFrame(NodeId.ToByteArray()) - .SendMoreFrame(ConvertOp(MessageOp.Identify)) - .SendFrame(message[2].Buffer); - break; + await Task.Delay(TimeSpan.FromMinutes(5)); + var peers = GetActivePeers(); + _logger.LogInformation($"Check in - Outgoing peers: {peers.Count(x => x.Outgoing)}"); + _logger.LogInformation($"Check in - Incoming peers: {peers.Count(x => !x.Outgoing)}"); + + //var process = Process.GetCurrentProcess(); + + //_logger.LogInformation($"Check in - Thread count: {process.Threads.Count}"); + //_logger.LogInformation($"Check in - Working set: {process.WorkingSet64}"); + //_logger.LogInformation($"Check in - PrivateMemorySize: {process.PrivateMemorySize64}"); - case MessageOp.Disconnect: - _logger.LogDebug("Recv disconnect from {0}", clientId); - _incomingPeerLastContact.TryRemove(clientId, out var v); - break; } + }); + } + + private void AttachEventHandlers(PeerConnection peer) + { + peer.OnReceiveMessage += Peer_OnReceiveMessage; + peer.OnDisconnect += Peer_OnDisconnect; + peer.OnPeerException += Peer_OnPeerException; + peer.OnUnresponsive += Peer_OnUnresponsive; + peer.OnIdentify += Peer_OnIdentify; + } + + private void Peer_OnIdentify(PeerConnection sender) + { + _logger.LogInformation($"Peer identify {sender.RemoteEndPoint} - {sender.RemoteId}"); + + if (!string.IsNullOrEmpty(sender.ConnectionString)) + { + var peers = _peerRoundRobin.Where(x => x.ConnectionString == sender.ConnectionString); + foreach (var peer in peers) + peer.NodeId = sender.RemoteId; } - catch (Exception ex) + + //remove duplicate connections + lock (_duplicateLock) { - _logger.LogError($"Error processing serv message, {ex.Message}"); + var peers = GetActivePeers(); + foreach (var peer in peers.Where(x => x.RemoteId == sender.RemoteId && x != sender)) + { + peer.Disconnect(); + peer.Close(); + } } - } - private async void Peer_ReceiveReady(object sender, NetMQSocketEventArgs e) - { - try + //remove connection to self + if (sender.RemoteId == NodeId) { - var message = e.Socket.ReceiveMultipartMessage(); - if (message.FrameCount < 2) + if (!string.IsNullOrEmpty(sender.ConnectionString)) { - return; + var selfs = _peerRoundRobin.Where(x => x.ConnectionString == sender.ConnectionString); + foreach (var self in selfs) + self.IsSelf = true; } + sender.Disconnect(); + sender.Close(); + } + } - var serverId = new Guid(message[0].Buffer); - var op = (MessageOp) (message[1].Buffer.First()); + private void Peer_OnUnresponsive(PeerConnection sender) + { + _logger.LogInformation($"Unresponsive peer {sender.RemoteEndPoint}"); + } - switch (op) + private void Peer_OnPeerException(PeerConnection sender, Exception exception) + { + _logger.LogError($"Peer exception {sender.RemoteEndPoint} - {exception.Message}"); + } + + private async void Peer_OnReceiveMessage(PeerConnection sender, byte command, byte[] data) + { + try + { + switch (command) { - case MessageOp.Block: - await ProcessBlock(message, serverId, false); + case Commands.Block: + await ProcessBlock(data, sender.RemoteId, false); break; - case MessageOp.Tail: - await ProcessBlock(message, serverId, true); + case Commands.BlockRequest: + await ProcessBlockRequest(data, false, sender); break; - case MessageOp.Txn: - await ProcessTransaction(message, serverId); + case Commands.NextBlockRequest: + await ProcessBlockRequest(data, true, sender); break; - case MessageOp.BlockRequest: - await ProcessBlockRequest(message, e.Socket, null); + case Commands.BlockHeightRequest: + await ProcessBlockRequest(BitConverter.ToUInt32(data, 0), sender); break; - case MessageOp.PeerShare: - if (IsSharablePeer(message[2].ConvertToString())) - AddPeer(new KnownPeer() { ConnectionString = message[2].ConvertToString() }); + case Commands.PeerShare: + if (IsSharablePeer(Encoding.UTF8.GetString(data))) + AddPeer(new KnownPeer() { ConnectionString = Encoding.UTF8.GetString(data) }); break; - - case MessageOp.Identify: - _logger.LogDebug("Recv identify from {0}", serverId); - _outgoingSockets[serverId] = e.Socket; - var peerConStr = message[2].ConvertToString(); - _outgoingConnectionStrings[peerConStr] = serverId; - foreach (var prr in _peerRoundRobin.Where(x => x.ConnectionString == peerConStr).ToList()) - prr.LastContact = DateTime.Now; + case Commands.Tail: + await ProcessBlock(data, sender.RemoteId, true); break; - - case MessageOp.Disconnect: - _logger.LogDebug("Recv disconnect from {0}", serverId); - _poller.Remove(e.Socket); - e.Socket.Close(); - _outgoingSockets.TryRemove(serverId, out var sock); + case Commands.Txn: + await ProcessTransaction(data, sender.RemoteId); + break; + case Commands.TxnRequest: + ProcessTxnRequest(sender); break; } } catch (Exception ex) { - _logger.LogError($"Error processing peer message, {ex.Message}"); + _logger.LogError($"Error procssing command {command} - {ex.Message}"); } } - private async Task ProcessBlockRequest(NetMQMessage message, IOutgoingSocket socket, Guid? peerId) + private async Task ProcessBlockRequest(byte[] blockId, bool next, PeerConnection peer) { - var prevBlockId = message[2].Buffer; - var block = await _blockRepository.GetNextBlock(prevBlockId); + Block block = null; + if (next) + block = await _blockRepository.GetNextBlock(blockId); + else + block = await _blockRepository.GetBlock(blockId); + if (block != null) { - _logger.LogDebug("Responding to block request"); + _logger.LogInformation($"Responding to block request {next} {BitConverter.ToString(blockId)}"); var data = SerializeObject(block); - SendBlock(socket, false, peerId, data, -1); + SendBlock(peer, data, false); } else { - _logger.LogDebug("Unable to respond to block request"); + _logger.LogInformation($"Unable to respond to block request {next} {BitConverter.ToString(blockId)}"); } } - private async Task ProcessBlock(NetMQMessage message, Guid originId, bool tail) + private async Task ProcessBlockRequest(uint height, PeerConnection peer) { - _logger.LogDebug($"Processing block {tail}"); - var hopCount = message[2].ConvertToInt32(); - var block = DeserializeObject(message[3].Buffer); - var result = PeerDataResult.Ignore; - if (tail) - result = await _blockReciever.RecieveTail(block); + var header = await _blockRepository.GetPrimaryHeader(height); + + if (header != null) + { + _logger.LogInformation($"Responding to block request {height} with {BitConverter.ToString(header.BlockId)}"); + var block = await _blockRepository.GetBlock(header.BlockId); + var data = SerializeObject(block); + SendBlock(peer, data, false); + } else - result = await _blockReciever.RecieveBlock(block); + { + _logger.LogInformation($"Unable to respond to block request {height}"); + } + } + + private async Task ProcessBlock(byte[] data, Guid originId, bool tip) + { + var block = DeserializeObject(data); + + _logger.LogDebug($"Recv block {BitConverter.ToString(block.Header.BlockId)} from {originId}"); + + var result = PeerDataResult.Ignore; + result = await _reciever.RecieveBlock(block); - if ((tail) && (result == PeerDataResult.Relay) && (hopCount > -1)) + if ((tip) && (result == PeerDataResult.Relay)) { - var incomingTask = Task.Factory.StartNew(() => + var relayTask = Task.Factory.StartNew(() => { - var peerList = GetIncomingPeers().Where(x => x != originId); - Parallel.ForEach(peerList, peerId => + var peerList = GetActivePeers().Where(x => x.RemoteId != originId); + Parallel.ForEach(peerList, peer => { - SendBlock(_incomingSocket, tail, peerId, message[3].Buffer, hopCount + 1); + SendBlock(peer, data, tip); }); }); + } - var outgoingTask = Task.Factory.StartNew(() => - { - var peerList = GetOutgoingPeers().Where(x => x != originId); - Parallel.ForEach(peerList, peerId => - { - SendBlock(_outgoingSockets[peerId], tail, null, message[3].Buffer, hopCount + 1); - }); - }); + if (result == PeerDataResult.Demerit) + { + PeerConnection peer = GetActivePeers().FirstOrDefault(x => x.RemoteId == originId); + if (peer != null) + peer.DemeritPoints++; } } - private async Task ProcessTransaction(NetMQMessage message, Guid originId) + private async Task ProcessTransaction(byte[] data, Guid originId) { - var hopCount = message[2].ConvertToInt32(); - var txn = DeserializeObject(message[3].Buffer); - var result = await _transactionReciever.RecieveTransaction(txn); + var txn = DeserializeObject(data); + var result = await _reciever.RecieveTransaction(txn); - if ((result == PeerDataResult.Relay) && (hopCount > -1)) + if (result == PeerDataResult.Relay) { - var incomingTask = Task.Factory.StartNew(() => + var relayTask = Task.Factory.StartNew(() => { - var peerList = GetIncomingPeers().Where(x => x != originId); - Parallel.ForEach(peerList, peerId => + var peerList = GetActivePeers().Where(x => x.RemoteId != originId); + Parallel.ForEach(peerList, peer => { - SendTxn(_incomingSocket, peerId, message[3].Buffer, hopCount + 1); + SendTxn(peer, data); }); }); + } - var outgoingTask = Task.Factory.StartNew(() => - { - var peerList = GetOutgoingPeers().Where(x => x != originId); - Parallel.ForEach(peerList, peerId => - { - SendTxn(_outgoingSockets[peerId], null, message[3].Buffer, hopCount + 1); - }); - }); + if (result == PeerDataResult.Demerit) + { + PeerConnection peer = GetActivePeers().FirstOrDefault(x => x.RemoteId == originId); + if (peer != null) + peer.DemeritPoints++; } } - public void Open() + private void ProcessTxnRequest(PeerConnection peer) { - _incomingSocket.Bind($"tcp://*:{_port}"); - _poller.Add(_incomingSocket); - _poller.RunAsync(); - _houseKeeper = new NetMQTimer(TimeSpan.FromSeconds(30)); - _houseKeeper.Elapsed += HouseKeeper_Elapsed; - _poller.Add(_houseKeeper); - _houseKeeper.Enable = true; - DiscoverPeers(); - DiscoverOwnConnectionStrings(); - AdvertiseToPeers(); + var txns = _unconfirmedTransactionPool.Get; + + foreach (var txn in txns) + { + var data = SerializeObject(txn); + SendTxn(peer, data); + } } - private void OnboardPeer(string connStr) + private void Peer_OnDisconnect(PeerConnection sender) { + _peerEvent.WaitOne(); try { - var peer = new DealerSocket(); - peer.Options.Identity = NodeId.ToByteArray(); - peer.ReceiveReady += Peer_ReceiveReady; - peer.Connect(connStr); - _poller.Add(peer); - peer.SendMoreFrame(ConvertOp(MessageOp.Connect)) - .SendFrame(connStr); + _logger.LogInformation($"Peer disconnect {sender.RemoteId} {sender.RemoteEndPoint}"); + _peerConnections.Remove(sender); + sender.Close(); } catch (Exception ex) { - _logger.LogError($"Error connecting to {connStr} - {ex.Message}"); + _logger.LogError($"Disconnect error ({sender.RemoteId}) - {ex.Message}"); + } + finally + { + _peerEvent.Set(); } } - public void Close() + private async Task OnboardPeer(string connStr) { - foreach (var peerId in GetIncomingPeers()) + try { - _incomingSocket - .SendMoreFrame(peerId.ToByteArray()) - .SendMoreFrame(NodeId.ToByteArray()) - .SendFrame(ConvertOp(MessageOp.Disconnect)); + var peer = new PeerConnection(_serviceId, _version, NodeId); + await peer.Connect(connStr); + AttachEventHandlers(peer); + _peerEvent.WaitOne(); + try + { + _peerConnections.Add(peer); + peer.Run(); + } + finally + { + _peerEvent.Set(); + } } - - foreach (var peerId in GetOutgoingPeers()) + catch (Exception ex) { - _outgoingSockets[peerId] - .SendFrame(ConvertOp(MessageOp.Disconnect)); - - _poller.Remove(_outgoingSockets[peerId]); - _outgoingSockets[peerId].Close(); + _logger.LogError($"Error connecting to {connStr} - {ex.Message}"); + var peers = _peerRoundRobin.Where(x => x.ConnectionString == connStr); + foreach (var peer in peers) + peer.Unreachable = true; } + } - _outgoingSockets.Clear(); + public void Close() + { + _discoveryTimer.Dispose(); + _sharePeersTimer.Dispose(); + _listener?.Stop(); - _poller.Stop(); - _poller.Remove(_incomingSocket); - _poller.Remove(_houseKeeper); - _houseKeeper.Enable = false; - _incomingSocket.Close(); + foreach (var peer in GetActivePeers()) + { + peer.Disconnect(); + } } - public void DiscoverPeers() + public async Task DiscoverPeers() { - foreach (var discovery in _discoveryServices) + Parallel.ForEach(_discoveryServices, async discovery => { - Task.Factory.StartNew(async () => + try { - try - { - var newPeers = await discovery.DiscoverPeers(); - foreach (var np in newPeers) - AddPeer(np); - ConnectOut(); - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - } - }); - } + var newPeers = await discovery.DiscoverPeers(); + foreach (var np in newPeers) + AddPeer(np); + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + } + }); + await ConnectOut(); } private void AddPeer(KnownPeer newPeer) @@ -334,98 +425,70 @@ private async void SharePeers(object state) { foreach (var ds in _discoveryServices) await ds.SharePeers(_peerRoundRobin.ToList()); - - // } - private void HouseKeeper_Elapsed(object sender, NetMQTimerEventArgs e) + private async Task ConnectOut() { - _logger.LogDebug("Performing house keeping"); - ConnectOut(); - } - - private void ConnectOut() - { - var target = (TargetOutgoingCount - _outgoingSockets.Count); + var activePeers = GetActivePeers(); + var peersOut = activePeers.Where(x => x.Outgoing).ToList(); + var target = (TargetOutgoingCount - peersOut.Count()); if (target <= 0) return; var actual = 0; var counter = 0; - while ((actual < target) && (counter < _peerRoundRobin.Count)) + _connectOutEvent.WaitOne(); + try { - if (_peerRoundRobin.TryDequeue(out var kp)) + while ((actual < target) && (counter < _peerRoundRobin.Count)) { - _peerRoundRobin.Enqueue(kp); counter++; - if (_outgoingConnectionStrings.ContainsKey(kp.ConnectionString)) - { - if (_outgoingSockets.ContainsKey(_outgoingConnectionStrings[kp.ConnectionString])) - continue; - } - _logger.LogDebug($"Connecting to {kp.ConnectionString}"); - OnboardPeer(kp.ConnectionString); - actual++; - } - } - } - public void RegisterBlockReceiver(IBlockReceiver blockReceiver) - { - _blockReciever = blockReceiver; - } + if (!_peerRoundRobin.TryDequeue(out var kp)) + continue; - public void RegisterTransactionReceiver(ITransactionReceiver transactionReciever) - { - _transactionReciever = transactionReciever; + _peerRoundRobin.Enqueue(kp); + + if (kp.IsSelf) + continue; + + if (peersOut.Any(x => x.ConnectionString == kp.ConnectionString)) + continue; + + if (activePeers.Any(x => x.RemoteId == kp.NodeId)) + continue; + + _logger.LogInformation($"Connecting to {kp.ConnectionString}"); + await OnboardPeer(kp.ConnectionString); + actual++; + } + } + finally + { + _connectOutEvent.Set(); + } } - public void BroadcastTail(Block block) { var data = SerializeObject(block); - var incoming = GetIncomingPeers(); - var outgoing = GetOutgoingPeers(); - - Task.Factory.StartNew(() => - { - Parallel.ForEach(incoming, peerId => - { - SendBlock(_incomingSocket, true, peerId, data, 0); - }); - }); + var peers = GetActivePeers().Where(x => x.RemoteId != NodeId); Task.Factory.StartNew(() => { - Parallel.ForEach(outgoing, peerId => + Parallel.ForEach(peers, peer => { - SendBlock(_outgoingSockets[peerId], true, null, data, 0); + SendBlock(peer, data, true); }); }); } - private void SendBlock(IOutgoingSocket socket, bool tail, Guid? peerId, byte[] data, int hopCount) + private void SendBlock(PeerConnection peer, byte[] data, bool tail) { try { - var op = ConvertOp(MessageOp.Block); - if (tail) - op = ConvertOp(MessageOp.Tail); - - var msg = new NetMQMessage(); - - if ((peerId.HasValue)) - { - msg.Append(peerId.Value.ToByteArray()); - msg.Append(NodeId.ToByteArray()); - } - - msg.Append(op); - msg.Append(BitConverter.GetBytes(hopCount)); - msg.Append(data); - - socket.SendMultipartMessage(msg); + peer.Send(tail ? Commands.Tail : Commands.Block, data); } catch (Exception ex) { @@ -433,48 +496,25 @@ private void SendBlock(IOutgoingSocket socket, bool tail, Guid? peerId, byte[] d } } - public void BroadcastTransaction(TransactionEnvelope transaction) + public void BroadcastTransaction(Transaction transaction) { var data = SerializeObject(transaction); - - Task.Factory.StartNew(() => - { - var incoming = GetIncomingPeers(); - Parallel.ForEach(incoming, peerId => - { - SendTxn(_incomingSocket, peerId, data, 0); - }); - }); Task.Factory.StartNew(() => { - var outgoing = GetOutgoingPeers(); - Parallel.ForEach(outgoing, peerId => + var peers = GetActivePeers().Where(x => x.RemoteId != NodeId); + Parallel.ForEach(peers, peer => { - SendTxn(_outgoingSockets[peerId], null, data, 0); + SendTxn(peer, data); }); }); } - private void SendTxn(IOutgoingSocket socket, Guid? peerId, byte[] data, int hopCount) + private void SendTxn(PeerConnection peer, byte[] data) { try { - var op = ConvertOp(MessageOp.Txn); - - var msg = new NetMQMessage(); - - if ((peerId.HasValue) && (socket is RouterSocket)) - { - msg.Append(peerId.Value.ToByteArray()); - msg.Append(NodeId.ToByteArray()); - } - - msg.Append(op); - msg.Append(BitConverter.GetBytes(hopCount)); - msg.Append(data); - - socket.SendMultipartMessage(msg); + peer.Send(Commands.Txn, data); } catch (Exception ex) { @@ -484,65 +524,78 @@ private void SendTxn(IOutgoingSocket socket, Guid? peerId, byte[] data, int hopC public void RequestNextBlock(byte[] blockId) { - Task.Factory.StartNew(async () => + LoadBalancedRequest((peer) => { - var incoming = GetIncomingPeers(); - foreach (var peerId in incoming) - { - _logger.LogDebug($"Requesting block from incoming peer {peerId}"); - _incomingSocket - .SendMoreFrame(peerId.ToByteArray()) - .SendMoreFrame(NodeId.ToByteArray()) - .SendMoreFrame(ConvertOp(MessageOp.BlockRequest)) - .SendFrame(blockId); + _logger.LogInformation($"Requesting next block {BitConverter.ToString(blockId)} from peer {peer.RemoteId}"); + peer.Send(Commands.NextBlockRequest, blockId); + return Task.CompletedTask; + }, + async () => (await _blockRepository.GetBlockHeader(blockId)) != null); + } - await Task.Delay(TimeSpan.FromSeconds(30)); + public void RequestBlock(byte[] blockId) + { + LoadBalancedRequest((peer) => + { + _logger.LogInformation($"Requesting block {BitConverter.ToString(blockId)} from peer {peer.RemoteId}"); + peer.Send(Commands.BlockRequest, blockId); + return Task.CompletedTask; + }, + async () => (await _blockRepository.GetBlockHeader(blockId)) != null); + } - if ((await _blockRepository.GetNextBlock(blockId)) != null) - return; - } + public void RequestBlockByHeight(uint height) + { + LoadBalancedRequest((peer) => + { + _logger.LogInformation($"Requesting block {height} from peer {peer.RemoteId}"); + peer.Send(Commands.BlockHeightRequest, BitConverter.GetBytes(height)); + return Task.CompletedTask; + }, + async () => (await _blockRepository.GetPrimaryHeader(height)) != null); + } - var outgoing = GetOutgoingPeers(); - foreach (var peerId in outgoing) + public void LoadBalancedRequest(Func requestAction, Func> resolveAction) + { + Task.Factory.StartNew(async () => + { + var peers = GetActivePeers() + .Where(x => x.RemoteId != NodeId && x.LastContact.HasValue) + .Where(x => x.LastContact > (DateTime.Now.AddMinutes(-20))) + .OrderBy(x => x.RequestCount); + //TODO: round robin + foreach (var peer in peers) { - _logger.LogDebug($"Requesting block from outgoing peer {peerId}"); - _outgoingSockets[peerId] - .SendMoreFrame(ConvertOp(MessageOp.BlockRequest)) - .SendFrame(blockId); - - await Task.Delay(TimeSpan.FromSeconds(30)); - - if ((await _blockRepository.GetNextBlock(blockId)) != null) + peer.RequestCount++; + await requestAction(peer); + await Task.Delay(TimeSpan.FromSeconds(5)); + if (await resolveAction()) return; } }); } - public void Dispose() { - - } - - private ICollection GetIncomingPeers() - { - return _incomingPeerLastContact.Select(x => x.Key).ToList(); - } - private ICollection GetOutgoingPeers() - { - return _outgoingSockets.Keys; } private void AdvertiseToPeers() { foreach (var ds in _discoveryServices) - Task.Factory.StartNew(() => + Task.Factory.StartNew(() => { - if (_internalConnsctionString != null) - ds.AdvertiseLocal(_internalConnsctionString); + try + { + if (_internalConnectionString != null) + ds.AdvertiseLocal(_internalConnectionString); - if (_externalConnsctionString != null) - ds.AdvertiseGlobal(_externalConnsctionString); + if (_externalConnectionString != null) + ds.AdvertiseGlobal(_externalConnectionString); + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + } }); } @@ -550,9 +603,11 @@ private void DiscoverOwnConnectionStrings() { var ownAddress = _ownAddressResolver.ResolvePreferredLocalAddress(); if (ownAddress != null) - _internalConnsctionString = $"tcp://{ownAddress}:{_port}"; + _internalConnectionString = $"tcp://{ownAddress}:{_port}"; - //TODO: external addresses + var externalConnectionString = _natTraversal.ConfigureNatTraversal(ownAddress, (int)_port); + if (externalConnectionString != null) + _externalConnectionString = $"tcp://{externalConnectionString}"; } private static byte[] SerializeObject(object data) @@ -614,14 +669,33 @@ private static bool IsSharablePeer(string connectionUri) } } - private byte[] ConvertOp(MessageOp op) + public ICollection GetPeersIn() { - byte[] result = new byte[1]; - result[0] = (byte)op; - return result; + return GetActivePeers() + .Where(x => !x.Outgoing) + .Select(x => new ConnectedPeer(x.RemoteId, x.RemoteEndPoint?.ToString())) + .ToList(); } - enum MessageOp { Disconnect = 0, Tail = 1, Block = 2, Txn = 3, BlockRequest = 4, PeerShare = 5, Connect = 6, Identify = 7 } - + public ICollection GetPeersOut() + { + return GetActivePeers() + .Where(x => x.Outgoing) + .Select(x => new ConnectedPeer(x.RemoteId, x.RemoteEndPoint?.ToString())) + .ToList(); + } + + } + + internal class Commands + { + public const byte Tail = 0; + public const byte Block = 1; + public const byte Txn = 2; + public const byte BlockRequest = 3; + public const byte NextBlockRequest = 4; + public const byte BlockHeightRequest = 5; + public const byte PeerShare = 6; + public const byte TxnRequest = 7; } } diff --git a/NBlockchain/Services/NodeHost.cs b/NBlockchain/Services/NodeHost.cs deleted file mode 100644 index 676b331..0000000 --- a/NBlockchain/Services/NodeHost.cs +++ /dev/null @@ -1,187 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.DependencyInjection; -using System.Collections.Concurrent; -using NBlockchain.Interfaces; -using NBlockchain.Models; - -namespace NBlockchain.Services -{ - public class NodeHost : INodeHost - { - private readonly INetworkParameters _parameters; - private readonly IBlockRepository _blockRepository; - private readonly IBlockVerifier _blockVerifier; - private readonly ILogger _logger; - private readonly IServiceProvider _serviceProvider; - private readonly IDateTimeProvider _dateTimeProvider; - private readonly ITransactionKeyResolver _transactionKeyResolver; - private readonly IPeerNetwork _peerNetwork; - private readonly AutoResetEvent _blockEvent = new AutoResetEvent(true); - private readonly IPendingTransactionList _pendingTransactionList; - private readonly IDifficultyCalculator _difficultyCalculator; - - private readonly Timer _pollTimer; - - - public NodeHost(IBlockRepository blockRepository, IBlockVerifier blockVerifier, ILoggerFactory loggerFactory, IServiceProvider serviceProvider, INetworkParameters parameters, IDateTimeProvider dateTimeProvider, ITransactionKeyResolver transactionKeyResolver, IPendingTransactionList pendingTransactionList, IPeerNetwork peerNetwork, IDifficultyCalculator difficultyCalculator) - { - _blockRepository = blockRepository; - _blockVerifier = blockVerifier; - _serviceProvider = serviceProvider; - _parameters = parameters; - _dateTimeProvider = dateTimeProvider; - _transactionKeyResolver = transactionKeyResolver; - _pendingTransactionList = pendingTransactionList; - _peerNetwork = peerNetwork; - _difficultyCalculator = difficultyCalculator; - _logger = loggerFactory.CreateLogger(); - - _peerNetwork.RegisterBlockReceiver(this); - _peerNetwork.RegisterTransactionReceiver(this); - - _pollTimer = new Timer(GetMissingBlocks, null, TimeSpan.FromSeconds(5), _parameters.BlockTime); - } - - public async Task RecieveTail(Block block) - { - _blockEvent.WaitOne(); - try - { - _logger.LogDebug($"Recv tail {BitConverter.ToString(block.Header.BlockId)}"); - - if (await _blockRepository.HaveBlock(block.Header.BlockId)) - return PeerDataResult.Ignore; - - var prevHeader = await _blockRepository.GetNewestBlockHeader(); - - if (prevHeader != null) - { - if (!block.Header.PreviousBlock.SequenceEqual(prevHeader.BlockId)) - return PeerDataResult.Ignore; - - var expectedDifficulty = await _difficultyCalculator.CalculateDifficulty(prevHeader.Timestamp); - - if (block.Header.Difficulty < expectedDifficulty) - return PeerDataResult.Ignore; - } - else - { - if (!(block.Header.PreviousBlock.SequenceEqual(Block.HeadKey) && await _blockRepository.IsEmpty())) - return PeerDataResult.Ignore; - } - - if (!_blockVerifier.Verify(block)) - { - _logger.LogWarning($"Block verification failed for {BitConverter.ToString(block.Header.BlockId)}"); - return PeerDataResult.Demerit; - } - - var contentTxnIds = block.Transactions.Select(x => _transactionKeyResolver.ResolveKey(x)).ToList(); - - //if (!_blockVerifier.VerifyContentThreshold(contentTxnIds, _pendingTransactionList.Get)) - //{ - // _logger.LogWarning($"Block content verification failed for {BitConverter.ToString(block.Header.BlockId)}"); - // return PeerDataResult.Ignore; - //} - - await _blockRepository.AddBlock(block); - _pendingTransactionList.Remove(block.Transactions); - - _logger.LogDebug($"Accepted tail {BitConverter.ToString(block.Header.BlockId)}"); - - return PeerDataResult.Relay; - } - finally - { - _blockEvent.Set(); - } - } - - public async Task RecieveBlock(Block block) - { - _blockEvent.WaitOne(); - try - { - _logger.LogDebug($"Recv block {BitConverter.ToString(block.Header.BlockId)}"); - - if (await _blockRepository.HaveBlock(block.Header.BlockId)) - return PeerDataResult.Ignore; - - if (!await _blockRepository.HaveBlock(block.Header.PreviousBlock)) - { - if (!(block.Header.PreviousBlock.SequenceEqual(Block.HeadKey) && await _blockRepository.IsEmpty())) - return PeerDataResult.Ignore; - } - - if (!_blockVerifier.Verify(block)) - { - _logger.LogWarning($"Block verification failed for {BitConverter.ToString(block.Header.BlockId)}"); - return PeerDataResult.Demerit; - } - - await _blockRepository.AddBlock(block); - - _logger.LogDebug($"Accepted block {BitConverter.ToString(block.Header.BlockId)}"); - } - finally - { - _blockEvent.Set(); - } - - GetMissingBlocks(null); - return PeerDataResult.Ignore; - } - - public Task RecieveTransaction(TransactionEnvelope transaction) - { - _logger.LogDebug($"Recv txn {transaction.OriginKey} from {transaction.Originator}"); - var txnResult = _blockVerifier.VerifyTransaction(transaction, _pendingTransactionList.Get); - - if (txnResult == 0) - { - //var txnKey = _transactionKeyResolver.ResolveKey(transaction); - if (_pendingTransactionList.Add(transaction)) - { - _logger.LogDebug($"Accepted txn {transaction.OriginKey} from {transaction.Originator}"); - return Task.FromResult(PeerDataResult.Relay); - } - - return Task.FromResult(PeerDataResult.Ignore); - } - _logger.LogDebug($"Rejected txn {transaction.OriginKey} from {transaction.Originator} code: {txnResult}"); - return Task.FromResult(PeerDataResult.Ignore); - } - - - public async Task SendTransaction(TransactionEnvelope transaction) - { - _logger.LogDebug("Sending txn"); - await RecieveTransaction(transaction); - _peerNetwork.BroadcastTransaction(transaction); - } - - private async void GetMissingBlocks(object state) - { - var prevHeader = await _blockRepository.GetNewestBlockHeader(); - - if (prevHeader == null) - { - _logger.LogDebug("Requesting head block"); - _peerNetwork.RequestNextBlock(Block.HeadKey); - return; - } - - if ((DateTime.UtcNow.Ticks - prevHeader.Timestamp) > _parameters.BlockTime.Ticks) - { - _logger.LogDebug($"Requesting missing block after {BitConverter.ToString(prevHeader.BlockId)}"); - _peerNetwork.RequestNextBlock(prevHeader.BlockId); - } - } - - } -} diff --git a/NBlockchain/Services/PeerDiscovery/MulticastDiscovery.cs b/NBlockchain/Services/PeerDiscovery/MulticastDiscovery.cs index f282ff4..bf1cd5b 100644 --- a/NBlockchain/Services/PeerDiscovery/MulticastDiscovery.cs +++ b/NBlockchain/Services/PeerDiscovery/MulticastDiscovery.cs @@ -39,6 +39,7 @@ public async Task AdvertiseGlobal(string connectionString) public async Task AdvertiseLocal(string connectionString) { + _logger.LogInformation($"Advertising to peers {connectionString}"); if (_advertiseTask != null) { _advertiseCts.Cancel(); @@ -64,56 +65,65 @@ public async Task AdvertiseLocal(string connectionString) await udpClient.SendAsync(data, data.Length, ipEndPoint); await Task.Delay(_interval); } - //udpClient.Close(); + udpClient.Dispose(); } } catch (Exception ex) { - _logger.LogError(ex.Message); + _logger.LogError($"Error advertising - {ex.Message}"); } }); } public async Task> DiscoverPeers() { - _logger.LogDebug("Discovering peers"); - var result = new HashSet(); - var udpClient = new UdpClient(_port); + _logger.LogInformation("Discovering peers"); - var ownAddress = _ownAddressResolver.ResolvePreferredLocalAddress(); - udpClient.JoinMulticastGroup(IPAddress.Parse(_multicastAddress), ownAddress); + var result = new HashSet(); - udpClient.Client.ReceiveTimeout = Convert.ToInt32(_interval.TotalMilliseconds + 1000); + try + { + var udpClient = new UdpClient(_port); - DateTime pollUntil = DateTime.Now.Add(_interval); + var ownAddress = _ownAddressResolver.ResolvePreferredLocalAddress(); + udpClient.JoinMulticastGroup(IPAddress.Parse(_multicastAddress), ownAddress); - while (pollUntil > DateTime.Now) - { - byte[] b = new byte[1024]; - try + udpClient.Client.ReceiveTimeout = Convert.ToInt32(_interval.TotalMilliseconds + 1000); + + DateTime pollUntil = DateTime.Now.Add(_interval); + + while (pollUntil > DateTime.Now) { - //var ipEndPoint = new IPEndPoint(IPAddress.Any, 0); - var data = await udpClient.ReceiveAsync(); //(ref ipEndPoint); - string message = Encoding.ASCII.GetString(data.Buffer); - _logger.LogDebug($"rx message {message}"); - if (message.StartsWith(_serviceId)) + byte[] b = new byte[1024]; + try { - var connStr = message.Remove(0, _serviceId.Length); - result.Add(new KnownPeer() + //var ipEndPoint = new IPEndPoint(IPAddress.Any, 0); + var data = await udpClient.ReceiveAsync(); //(ref ipEndPoint); + string message = Encoding.ASCII.GetString(data.Buffer); + _logger.LogDebug($"rx message {message}"); + if (message.StartsWith(_serviceId)) { - ConnectionString = connStr, - LastContact = DateTime.Now - }); + var connStr = message.Remove(0, _serviceId.Length); + result.Add(new KnownPeer() + { + ConnectionString = connStr, + LastContact = DateTime.Now + }); + } + } + catch (SocketException ex) + { + _logger.LogError($"SocketException polling for peers - {ex.Message}"); + } + catch (Exception ex) + { + _logger.LogError($"Exception polling for peers - {ex.Message}"); } } - catch (SocketException ex) - { - _logger.LogDebug(ex.Message); - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - } + } + catch (Exception ex) + { + _logger.LogError($"Error joining multicast - {ex.Message}"); } return result; diff --git a/NBlockchain/Services/ProofOfWorkBlockNotary.cs b/NBlockchain/Services/ProofOfWorkConsensus.cs similarity index 78% rename from NBlockchain/Services/ProofOfWorkBlockNotary.cs rename to NBlockchain/Services/ProofOfWorkConsensus.cs index 9f11875..f3c769e 100644 --- a/NBlockchain/Services/ProofOfWorkBlockNotary.cs +++ b/NBlockchain/Services/ProofOfWorkConsensus.cs @@ -9,21 +9,26 @@ namespace NBlockchain.Services { - public class ProofOfWorkBlockNotary : IBlockNotary + public class ProofOfWorkConsensus: IConsensusMethod { private readonly IHasher _hasher; private readonly INetworkParameters _networkParameters; private readonly IHashTester _hashTester; private readonly AutoResetEvent _lock = new AutoResetEvent(true); - public ProofOfWorkBlockNotary(IHasher hasher, INetworkParameters networkParameters, IHashTester hashTester) + public ProofOfWorkConsensus(IHasher hasher, INetworkParameters networkParameters, IHashTester hashTester) { _hasher = hasher; _networkParameters = networkParameters; _hashTester = hashTester; } - public async Task ConfirmBlock(Block block, CancellationToken cancellationToken) + public bool VerifyConsensus(Block block) + { + return _hashTester.TestHash(block.Header.BlockId, block.Header.Difficulty); + } + + public async Task BuildConsensus(Block block, CancellationToken cancellationToken) { long counter = 0; var cancellationTokenSource = new CancellationTokenSource(); @@ -31,8 +36,8 @@ public async Task ConfirmBlock(Block block, CancellationToken cancellationToken) var opts = new ExecutionDataflowBlockOptions() { - MaxDegreeOfParallelism = Environment.ProcessorCount, - BoundedCapacity = Environment.ProcessorCount + 1 + MaxDegreeOfParallelism = 1, //Environment.ProcessorCount, + BoundedCapacity = 2 //Environment.ProcessorCount + 1 }; var actionBlock = new ActionBlock(nonce => VerifyForNonce(block.Header, nonce, cancellationTokenSource), opts); @@ -59,6 +64,7 @@ private void VerifyForNonce(BlockHeader header, long nonce, CancellationTokenSou { if (header.Status == BlockStatus.Unconfirmed) { + header.Timestamp = DateTime.UtcNow.Ticks; header.BlockId = hash; header.Nonce = nonce; header.Status = BlockStatus.Confirmed; diff --git a/NBlockchain/Services/Receiver.cs b/NBlockchain/Services/Receiver.cs new file mode 100644 index 0000000..1c3b890 --- /dev/null +++ b/NBlockchain/Services/Receiver.cs @@ -0,0 +1,25 @@ +using NBlockchain.Interfaces; +using System; +using System.Collections.Generic; +using System.Text; +using NBlockchain.Models; +using System.Threading.Tasks; + +namespace NBlockchain.Services +{ + public class Receiver : IReceiver + { + public event ReceiveBlock OnReceiveBlock; + public event RecieveTransaction OnRecieveTransaction; + + public Task RecieveBlock(Block block) + { + return OnReceiveBlock?.Invoke(block); + } + + public Task RecieveTransaction(Transaction transaction) + { + return OnRecieveTransaction?.Invoke(transaction); + } + } +} diff --git a/NBlockchain/Services/TransactionBuilder.cs b/NBlockchain/Services/TransactionBuilder.cs new file mode 100644 index 0000000..4beaa59 --- /dev/null +++ b/NBlockchain/Services/TransactionBuilder.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; +using NBlockchain.Interfaces; +using NBlockchain.Models; +using System.Linq; +using System.Threading.Tasks; + +namespace NBlockchain.Services +{ + public class TransactionBuilder : ITransactionBuilder + { + private readonly ITransactionKeyResolver _keyResolver; + + public TransactionBuilder(ITransactionKeyResolver keyResolver) + { + _keyResolver = keyResolver; + } + + public async Task Build(ICollection instructions) + { + var result = new Transaction(instructions); + result.TransactionId = await _keyResolver.ResolveKey(result); + return result; + } + } +} diff --git a/NBlockchain/Services/TransactionKeyResolver.cs b/NBlockchain/Services/TransactionKeyResolver.cs index fa60ef8..75fc7cc 100644 --- a/NBlockchain/Services/TransactionKeyResolver.cs +++ b/NBlockchain/Services/TransactionKeyResolver.cs @@ -13,21 +13,18 @@ namespace NBlockchain.Services public class TransactionKeyResolver : ITransactionKeyResolver { private readonly IHasher _hasher; + private readonly IMerkleTreeBuilder _merkleTreeBuilder; - public TransactionKeyResolver(IHasher hasher) + public TransactionKeyResolver(IHasher hasher, IMerkleTreeBuilder merkleTreeBuilder) { _hasher = hasher; + _merkleTreeBuilder = merkleTreeBuilder; } - public byte[] ResolveKey(TransactionEnvelope txn) + public async Task ResolveKey(Transaction txn) { - var txnStr = JsonConvert.SerializeObject(txn.Transaction, Formatting.None); - - var seed = txn.OriginKey.ToByteArray() - .Concat(Encoding.Unicode.GetBytes(txn.Originator)) - .Concat(Encoding.Unicode.GetBytes(txnStr)); - - return _hasher.ComputeHash(seed.ToArray()); + var tree = await _merkleTreeBuilder.BuildTree(txn.Instructions.Select(x => x.InstructionId).ToList()); + return tree.Value; } } } diff --git a/NBlockchain/Services/TransactionRule.cs b/NBlockchain/Services/TransactionRule.cs deleted file mode 100644 index 66afa30..0000000 --- a/NBlockchain/Services/TransactionRule.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using NBlockchain.Interfaces; -using NBlockchain.Models; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace NBlockchain.Services -{ - public abstract class TransactionRule : ITransactionRule - where T : class - { - public string TransactionType { get; } - - protected TransactionRule() - { - var attr = typeof(T).GetTypeInfo().GetCustomAttribute(); - TransactionType = attr.TypeId; - } - - public int Validate(TransactionEnvelope transaction, ICollection siblings) - { - if (!(transaction is T)) - return -5; - - return Validate(transaction, transaction.Transaction as T, siblings); - } - - protected abstract int Validate(TransactionEnvelope envelope, T transaction, ICollection siblings); - } -} diff --git a/NBlockchain/Services/PendingTransactionList.cs b/NBlockchain/Services/UnconfirmedTransactionPool.cs similarity index 69% rename from NBlockchain/Services/PendingTransactionList.cs rename to NBlockchain/Services/UnconfirmedTransactionPool.cs index b07465d..ef48bf9 100644 --- a/NBlockchain/Services/PendingTransactionList.cs +++ b/NBlockchain/Services/UnconfirmedTransactionPool.cs @@ -8,19 +8,19 @@ namespace NBlockchain.Services { - public class PendingTransactionList : IPendingTransactionList + public class UnconfirmedTransactionPool : IUnconfirmedTransactionPool { - private readonly ICollection _list = new List(); + private readonly ICollection _list = new List(); private readonly AutoResetEvent _evt = new AutoResetEvent(true); - public ICollection Get + public ICollection Get { get { _evt.WaitOne(); try { - var result = new List(); + var result = new List(); result.AddRange(_list); return result; } @@ -33,12 +33,12 @@ public ICollection Get public event EventHandler Changed; - public bool Add(TransactionEnvelope txn) + public bool Add(Transaction txn) { _evt.WaitOne(); try { - if (_list.Any(x => x.OriginKey == txn.OriginKey && x.Originator == x.Originator)) + if (_list.Any(x => txn.TransactionId.SequenceEqual(x.TransactionId))) return false; _list.Add(txn); @@ -51,14 +51,14 @@ public bool Add(TransactionEnvelope txn) } } - public void Remove(ICollection toRemove) + public void Remove(ICollection toRemove) { _evt.WaitOne(); try { foreach (var item in toRemove) { - var removals = _list.Where(x => x.OriginKey == item.OriginKey && x.Originator == item.Originator).ToList(); + var removals = _list.Where(x => item.TransactionId.SequenceEqual(x.TransactionId)).ToList(); foreach (var remove in removals) _list.Remove(remove); } diff --git a/Providers/NBlockchain.MongoDB/Models/PersistedBlock.cs b/Providers/NBlockchain.MongoDB/Models/PersistedBlock.cs index 2996c4f..29d9b95 100644 --- a/Providers/NBlockchain.MongoDB/Models/PersistedBlock.cs +++ b/Providers/NBlockchain.MongoDB/Models/PersistedBlock.cs @@ -2,23 +2,47 @@ using System.Collections.Generic; using System.Text; using MongoDB.Bson; +using NBlockchain.Interfaces; using NBlockchain.Models; +using NBlockchain.Services.Database; namespace NBlockchain.MongoDB.Models { - public class PersistedBlock : Block + public class PersistedBlock { public ObjectId Id { get; set; } + public BlockStatistics Statistics { get; set; } = new BlockStatistics(); + + public BlockHeader Header { get; set; } + public ICollection Transactions { get; set; } = new HashSet(); + public MerkleNode MerkleRootNode { get; set; } + public PersistedBlock() { } - public PersistedBlock(Block block) + public PersistedBlock(Block block, IAddressEncoder addressEncoder) { - this.Header = block.Header; - this.MerkleRootNode = block.MerkleRootNode; - this.Transactions = block.Transactions; + Header = block.Header; + MerkleRootNode = block.MerkleRootNode; + Statistics.TimeStamp = new DateTime(block.Header.Timestamp); + + foreach (var txn in block.Transactions) + Transactions.Add(new PersistedTransaction(txn, addressEncoder)); } + + public Block ToBlock() + { + var result = new Block(); + result.Header = Header; + result.MerkleRootNode = MerkleRootNode; + + foreach (var txn in Transactions) + result.Transactions.Add(txn.ToTransaction()); + + return result; + } + } } diff --git a/Providers/NBlockchain.MongoDB/Models/PersistedInstruction.cs b/Providers/NBlockchain.MongoDB/Models/PersistedInstruction.cs new file mode 100644 index 0000000..b79bd24 --- /dev/null +++ b/Providers/NBlockchain.MongoDB/Models/PersistedInstruction.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; +using MongoDB.Bson; +using NBlockchain.Interfaces; +using NBlockchain.Models; +using NBlockchain.Services.Database; + +namespace NBlockchain.MongoDB.Models +{ + public class PersistedInstruction : PersistedEntity + { + public PersistedInstruction(Instruction entity, IAddressEncoder addressEncoder) + : base(entity) + { + Id = entity.InstructionId; + Statistics.PublicKeyHash = addressEncoder.HashPublicKey(entity.PublicKey); + } + } +} diff --git a/Providers/NBlockchain.MongoDB/Models/PersistedTransaction.cs b/Providers/NBlockchain.MongoDB/Models/PersistedTransaction.cs new file mode 100644 index 0000000..8f85ccc --- /dev/null +++ b/Providers/NBlockchain.MongoDB/Models/PersistedTransaction.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Text; +using MongoDB.Bson; +using NBlockchain.Interfaces; +using NBlockchain.Models; +using NBlockchain.Services.Database; + +namespace NBlockchain.MongoDB.Models +{ + public class PersistedTransaction + { + public byte[] TransactionId { get; set; } + + public ICollection Instructions { get; set; } = new HashSet(); + + public PersistedTransaction() + { + } + + public PersistedTransaction(Transaction txn, IAddressEncoder addressEncoder) + { + TransactionId = txn.TransactionId; + foreach (var ins in txn.Instructions) + Instructions.Add(new PersistedInstruction(ins, addressEncoder)); + } + + public Transaction ToTransaction() + { + var result = new Transaction(); + result.TransactionId = TransactionId; + foreach (var ins in Instructions) + result.Instructions.Add(ins.Entity); + + return result; + } + + } +} \ No newline at end of file diff --git a/Providers/NBlockchain.MongoDB/NBlockchain.MongoDB.csproj b/Providers/NBlockchain.MongoDB/NBlockchain.MongoDB.csproj index b4ad546..e6e6d9f 100644 --- a/Providers/NBlockchain.MongoDB/NBlockchain.MongoDB.csproj +++ b/Providers/NBlockchain.MongoDB/NBlockchain.MongoDB.csproj @@ -2,14 +2,14 @@ netstandard2.0 - 0.1.0-alpha + 0.6.0-alpha MongoDB persistence store for NBlockchain https://github.com/danielgerlag/NBlockchain.git https://github.com/danielgerlag/NBlockchain git Blockchain - 0.1.0.0 - 0.1.0.0 + 0.6.0.0 + 0.6.0.0 true diff --git a/Providers/NBlockchain.MongoDB/README.md b/Providers/NBlockchain.MongoDB/README.md index cca611f..af2b3f7 100644 --- a/Providers/NBlockchain.MongoDB/README.md +++ b/Providers/NBlockchain.MongoDB/README.md @@ -4,20 +4,20 @@ Using Nuget package console ``` -PM> Install-Package NBlockchain.MongoDB -Version 0.1.0-alpha +PM> Install-Package NBlockchain.MongoDB -Version 0.5.0-alpha ``` Using .NET CLI ``` -dotnet add package NBlockchain.MongoDB --version 0.1.0-alpha +dotnet add package NBlockchain.MongoDB --version 0.5.0-alpha ``` ## Usage ```c# -services.AddBlockchain(x => -{ - x.UseMongoDB(@"mongodb://localhost:27017", "my-blockchain-db") - .UseTransactionRepository(); +services.AddBlockchain(blockchain => +{ + blockchain.UseMongoDB(@"mongodb://localhost:27017", "my-blockchain-db") + .UseInstructionRepository(); ... } -``` \ No newline at end of file +``` diff --git a/Providers/NBlockchain.MongoDB/ServiceCollectionExtensions.cs b/Providers/NBlockchain.MongoDB/ServiceCollectionExtensions.cs index e769983..edf732d 100644 --- a/Providers/NBlockchain.MongoDB/ServiceCollectionExtensions.cs +++ b/Providers/NBlockchain.MongoDB/ServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Text; using MongoDB.Driver; +using NBlockchain.Interfaces; using NBlockchain.MongoDB; using NBlockchain.MongoDB.Services; using NBlockchain.Models; @@ -19,6 +20,7 @@ public static BlockchainMongoOptions UseMongoDB(this BlockchainOptions options, }); options.UseBlockRepository(); + options.Services.AddTransient(); options.AddPeerDiscovery(); return new BlockchainMongoOptions(options, mongoUrl, databaseName); @@ -38,8 +40,8 @@ public BlockchainMongoOptions(BlockchainOptions options, string mongoUrl, string _databaseName = databaseName; } - public BlockchainMongoOptions UseTransactionRepository() - where TImplementation : MongoTransactionRepository, TService + public BlockchainMongoOptions UseInstructionRepository() + where TImplementation : MongoInstructionRepository, TService where TService : class { _options.Services.AddTransient(); diff --git a/Providers/NBlockchain.MongoDB/Services/MongoBlockRepository.cs b/Providers/NBlockchain.MongoDB/Services/MongoBlockRepository.cs index e2eb8d0..0a712bd 100644 --- a/Providers/NBlockchain.MongoDB/Services/MongoBlockRepository.cs +++ b/Providers/NBlockchain.MongoDB/Services/MongoBlockRepository.cs @@ -4,21 +4,25 @@ using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Bson.Serialization; +using MongoDB.Bson.IO; using MongoDB.Driver; using NBlockchain.MongoDB.Models; using NBlockchain.Interfaces; using NBlockchain.Models; +using System.IO; +using System.Collections.Generic; namespace NBlockchain.MongoDB.Services { public class MongoBlockRepository : IBlockRepository { private readonly IMongoDatabase _database; - //private readonly AutoResetEvent _cursorEvent = new AutoResetEvent(true); + private readonly IAddressEncoder _addressEncoder; - public MongoBlockRepository(IMongoDatabase database) + public MongoBlockRepository(IMongoDatabase database, IAddressEncoder addressEncoder) { _database = database; + _addressEncoder = addressEncoder; EnsureIndexes(); } @@ -29,59 +33,215 @@ static MongoBlockRepository() { foreach (var type in asm.ExportedTypes) { - var attr = type.GetTypeInfo().GetCustomAttribute(); + var attr = type.GetTypeInfo().GetCustomAttribute(); if (attr != null) { - BsonSerializer.RegisterDiscriminator(type, new BsonString(attr.TypeId)); + BsonSerializer.RegisterDiscriminator(type, new BsonString(attr.TypeIdentifier)); + //BsonSerializer.RegisterDiscriminator(type, TypeNameDiscriminator.GetDiscriminator(type)); + + //hack for dodgy mongo driver + IBsonWriter w = new BsonBinaryWriter(new MemoryStream()); + var constr = type.GetConstructor(new Type[0]); + BsonSerializer.Serialize(w, type, constr.Invoke(null)); } } } + + //BsonSerializer.RegisterDiscriminatorConvention(typeof(BlockTransaction), StandardDiscriminatorConvention.Scalar); + + + //TypeNameDiscriminator.GetDiscriminator + //TypeNameDiscriminator. + //BsonSerializer.RegisterDiscriminatorConvention(typeof(object), ObjectDiscriminatorConvention.Instance); //BsonSerializer.RegisterDiscriminator(t, t.FullName)); } - private IMongoCollection Blocks => _database.GetCollection("nbc.blocks"); + private IMongoCollection MainChain => _database.GetCollection("MainChain"); + private IMongoCollection ForkChain => _database.GetCollection("ForkChain"); public async Task AddBlock(Block block) - { - Blocks.InsertOne(new PersistedBlock(block)); + { + var persisted = new PersistedBlock(block, _addressEncoder); + var prevHeader = MainChain + .Find(x => x.Header.BlockId == block.Header.PreviousBlock) + .Project(x => x.Header) + .FirstOrDefault(); + + if (prevHeader != null) + persisted.Statistics.BlockTime = Convert.ToInt32(TimeSpan.FromTicks(block.Header.Timestamp - prevHeader.Timestamp).TotalSeconds); + + MainChain.InsertOne(persisted); + ForkChain.DeleteMany(x => x.Header.BlockId == block.Header.BlockId); + + await Task.Yield(); + } + + public Task HavePrimaryBlock(byte[] blockId) + { + var result = MainChain.Find(x => x.Header.BlockId == blockId).Any(); + return Task.FromResult(result); + } + + public Task HaveSecondaryBlock(byte[] blockId) + { + var result = ForkChain.Find(x => x.Header.BlockId == blockId).Any(); + return Task.FromResult(result); + } + + public Task IsEmpty() + { + return Task.FromResult(MainChain.Count(x => true) == 0); + } + + public Task GetBestBlockHeader() + { + if (MainChain.Count(x => true) == 0) + return Task.FromResult(null); + + var height = MainChain.AsQueryable().Max(x => x.Header.Height); + var query = MainChain.AsQueryable().Select(x => x.Header).Where(x => x.Height == height); + var result = query.FirstOrDefault(); + return Task.FromResult(result); } - public async Task HaveBlock(byte[] blockId) + public Task GetNextBlock(byte[] prevBlockId) + { + var persistedResult = MainChain.Find(x => x.Header.PreviousBlock == prevBlockId).FirstOrDefault(); + + if (persistedResult == null) + persistedResult = ForkChain.Find(x => x.Header.PreviousBlock == prevBlockId).FirstOrDefault(); + + return persistedResult == null ? Task.FromResult(null) : Task.FromResult(persistedResult.ToBlock()); + } + + public Task DiscardSecondaryBlock(byte[] blockId) + { + ForkChain.DeleteMany(x => x.Header.BlockId == blockId); + return Task.CompletedTask; + } + + public async Task GetAverageBlockTimeInSecs(DateTime startUtc, DateTime endUtc) { - var query = Blocks.Find(x => x.Header.BlockId == blockId); - return query.Any(); + var startTicks = startUtc.Ticks; + var endTicks = endUtc.Ticks; + + var avgQuery = MainChain.Aggregate() + .Match(x => x.Header.Timestamp > startTicks && x.Header.Timestamp < endTicks && x.Header.Height > 1) + .Group(new BsonDocument { { "_id", "$item" }, { "avg", new BsonDocument("$avg", "$Statistics.BlockTime") } }) + .SingleOrDefault(); + + if (avgQuery != null) + { + if (avgQuery.TryGetValue("avg", out var bOut)) + return Convert.ToInt32(bOut.AsDouble); + } + + return 0; + } + + public Task GetBlockHeader(byte[] blockId) + { + if (MainChain.Count(x => true) == 0) + return Task.FromResult(null); + + var result = MainChain.AsQueryable().Select(x => x.Header).Where(x => x.BlockId == blockId).FirstOrDefault(); + + if (result == null) + result = ForkChain.AsQueryable().Select(x => x.Header).Where(x => x.BlockId == blockId).FirstOrDefault(); + + return Task.FromResult(result); } - public async Task IsEmpty() + public Task GetBlock(byte[] blockId) { - return (Blocks.Count(x => true) == 0); + var persistedResult = MainChain.Find(x => x.Header.BlockId == blockId).FirstOrDefault(); + + if (persistedResult == null) + persistedResult = ForkChain.Find(x => x.Header.BlockId == blockId).FirstOrDefault(); + + return persistedResult == null ? Task.FromResult(null) : Task.FromResult(persistedResult.ToBlock()); } - public async Task GetNewestBlockHeader() + public Task GetPrimaryHeader(uint height) { - if (Blocks.Count(x => true) == 0) + if (MainChain.Count(x => true) == 0) + return Task.FromResult(null); + + var query = MainChain.AsQueryable().Select(x => x.Header).Where(x => x.Height == height); + var result = query.FirstOrDefault(); + return Task.FromResult(result); + } + + public async Task AddSecondaryBlock(Block block) + { + var persisted = new PersistedBlock(block, _addressEncoder); + ForkChain.InsertOne(persisted); + await Task.Yield(); + } + + public Task GetSecondaryHeader(byte[] forkBlockId) + { + var forktip = ForkChain.AsQueryable().Select(x => x.Header).Where(x => x.BlockId == forkBlockId).FirstOrDefault(); + return Task.FromResult(forktip); + } + + public async Task GetDivergentHeader(byte[] forkTipBlockId) + { + var forkHeader = await GetSecondaryHeader(forkTipBlockId); + if (forkHeader == null) return null; - var height = Blocks.AsQueryable().Max(x => x.Header.Height); - var query = Blocks.AsQueryable().Select(x => x.Header).Where(x => x.Height == height); - return query.FirstOrDefault(); + while (!forkHeader.PreviousBlock.SequenceEqual(Block.HeadKey)) + { + var mainParent = MainChain.AsQueryable().Select(x => x.Header).Where(x => x.BlockId == forkHeader.PreviousBlock).FirstOrDefault(); + if (mainParent != null) + return mainParent; + + forkHeader = await GetSecondaryHeader(forkHeader.PreviousBlock); + if (forkHeader == null) + return null; + } + return null; } - public async Task GetNextBlock(byte[] prevBlockId) + public async Task RewindChain(byte[] blockId) { - var query = Blocks.Find(x => x.Header.PreviousBlock == prevBlockId); - return query.FirstOrDefault(); + var divergent = MainChain.AsQueryable().FirstOrDefault(x => x.Header.BlockId == blockId); + if (divergent == null) + return; + + var archiveFork = MainChain.AsQueryable() + .Where(x => x.Header.Height > divergent.Header.Height) + .ToList() + .Select(x => x.ToBlock()); + + foreach (var block in archiveFork) + await AddSecondaryBlock(block); + + MainChain.DeleteMany(x => x.Header.Height > divergent.Header.Height); } - public async Task GetAverageBlockTime(DateTime startUtc, DateTime endUtc) + public async Task> GetFork(byte[] forkTipBlockId) { - throw new NotImplementedException(); - //var startTicks = startUtc.Ticks; - //var endTicks = endUtc.Ticks; - //var avg = Blocks.AsQueryable().Where(x => x.Header.Timestamp > startTicks && x.Header.Timestamp < endTicks && x.Header.Height > 1) - // .Average(x => (x.Header.Timestamp - (Blocks.AsQueryable().First(y => y.Header.BlockId.SequenceEqual(x.Header.PreviousBlock)).Header.Timestamp))); + var result = new List(); - //return Convert.ToInt64(avg); + var forkBlock = ForkChain.AsQueryable().FirstOrDefault(x => x.Header.BlockId == forkTipBlockId); + if (forkBlock == null) + return result; + + while (!forkBlock.Header.PreviousBlock.SequenceEqual(Block.HeadKey)) + { + result.Add(forkBlock.ToBlock()); + var mainParent = MainChain.AsQueryable().Select(x => x.Header).Where(x => x.BlockId == forkBlock.Header.PreviousBlock).FirstOrDefault(); + if (mainParent != null) + break; + + forkBlock = ForkChain.AsQueryable().FirstOrDefault(x => x.Header.BlockId == forkBlock.Header.PreviousBlock); + if (forkBlock == null) + break; + } + + return result.OrderBy(x => x.Header.Height).ToList(); } static bool _indexesCreated = false; @@ -90,15 +250,19 @@ private void EnsureIndexes() { if (!_indexesCreated) { - Blocks.Indexes.CreateOne(Builders.IndexKeys.Hashed(x => x.Header.BlockId), new CreateIndexOptions() { Background = true, Name = "idx_blockid" }); - Blocks.Indexes.CreateOne(Builders.IndexKeys.Ascending(x => x.Header.Height), new CreateIndexOptions() { Background = true, Name = "idx_height" }); - Blocks.Indexes.CreateOne(Builders.IndexKeys.Hashed(x => x.Header.PreviousBlock), new CreateIndexOptions() { Background = true, Name = "idx_prevblock" }); - //Blocks.Indexes.CreateOne(Builders.IndexKeys.Ascending(x => x.Transactions.Select(y => y.OriginKey)), new CreateIndexOptions() { Background = true, Name = "idx_origkey" }); - //Blocks.Indexes.CreateOne(Builders.IndexKeys.Hashed(x => x.Transactions.Select(y => y.Originator)), new CreateIndexOptions() { Background = true, Name = "idx_origin" }); + MainChain.Indexes.CreateOne(Builders.IndexKeys.Ascending(x => x.Header.BlockId), new CreateIndexOptions() { Background = true, Unique = true }); + MainChain.Indexes.CreateOne(Builders.IndexKeys.Ascending(x => x.Header.Height), new CreateIndexOptions() { Background = true, Unique = true }); + MainChain.Indexes.CreateOne(Builders.IndexKeys.Ascending(x => x.Header.PreviousBlock), new CreateIndexOptions() { Background = true, Unique = true }); + MainChain.Indexes.CreateOne(Builders.IndexKeys.Ascending(new StringFieldDefinition("Transactions.TransactionId")), new CreateIndexOptions() { Background = true }); + MainChain.Indexes.CreateOne(Builders.IndexKeys.Ascending(new StringFieldDefinition("Transactions.Instructions.Id")), new CreateIndexOptions() { Background = true }); + + ForkChain.Indexes.CreateOne(Builders.IndexKeys.Ascending(x => x.Header.BlockId), new CreateIndexOptions() { Background = true }); + ForkChain.Indexes.CreateOne(Builders.IndexKeys.Ascending(x => x.Header.Height), new CreateIndexOptions() { Background = true }); + ForkChain.Indexes.CreateOne(Builders.IndexKeys.Ascending(x => x.Header.PreviousBlock), new CreateIndexOptions() { Background = true }); _indexesCreated = true; } - } + } } } diff --git a/Providers/NBlockchain.MongoDB/Services/MongoInstructionRepository.cs b/Providers/NBlockchain.MongoDB/Services/MongoInstructionRepository.cs new file mode 100644 index 0000000..ace6d0f --- /dev/null +++ b/Providers/NBlockchain.MongoDB/Services/MongoInstructionRepository.cs @@ -0,0 +1,51 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; +using NBlockchain.MongoDB.Models; +using NBlockchain.Interfaces; +using NBlockchain.Models; + +namespace NBlockchain.MongoDB.Services +{ + public class MongoInstructionRepository : IInstructionRepository + { + protected readonly IMongoDatabase Database; + protected IMongoCollection MainChain => Database.GetCollection("MainChain"); + + public MongoInstructionRepository(IMongoDatabase database) + { + Database = database; + EnsureIndexes(); + } + + protected virtual void CreateIndexes() + { + } + + + static bool _indexesCreated = false; + private void EnsureIndexes() + { + if (!_indexesCreated) + { + CreateIndexes(); + _indexesCreated = true; + } + } + + public Task HaveInstruction(byte[] instructionId) + { + var filter = new FilterDefinitionBuilder() + .Eq(new StringFieldDefinition("Transactions.Instructions.Id"), instructionId); + + var result = MainChain.Find(filter).Any(); + + return Task.FromResult(result); + } + } + +} diff --git a/Providers/NBlockchain.MongoDB/Services/MongoPeerDirectory.cs b/Providers/NBlockchain.MongoDB/Services/MongoPeerDirectory.cs index e208792..fd1ca6f 100644 --- a/Providers/NBlockchain.MongoDB/Services/MongoPeerDirectory.cs +++ b/Providers/NBlockchain.MongoDB/Services/MongoPeerDirectory.cs @@ -76,6 +76,8 @@ public MongoPeerNode(KnownPeer node) { this.ConnectionString = node.ConnectionString; this.LastContact = node.LastContact; + this.IsSelf = node.IsSelf; + this.NodeId = node.NodeId; } public MongoPeerNode(ObjectId id, KnownPeer node) @@ -83,6 +85,8 @@ public MongoPeerNode(ObjectId id, KnownPeer node) this.Id = id; this.ConnectionString = node.ConnectionString; this.LastContact = node.LastContact; + this.IsSelf = node.IsSelf; + this.NodeId = node.NodeId; } } } diff --git a/Providers/NBlockchain.MongoDB/Services/MongoTransactionRepository.cs b/Providers/NBlockchain.MongoDB/Services/MongoTransactionRepository.cs deleted file mode 100644 index 7b9c9ee..0000000 --- a/Providers/NBlockchain.MongoDB/Services/MongoTransactionRepository.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Bson.Serialization; -using MongoDB.Driver; -using NBlockchain.MongoDB.Models; -using NBlockchain.Interfaces; -using NBlockchain.Models; - -namespace NBlockchain.MongoDB.Services -{ - public abstract class MongoTransactionRepository - { - protected readonly IMongoDatabase Database; - protected IMongoCollection Blocks => Database.GetCollection("nbc.blocks"); - - protected MongoTransactionRepository(IMongoDatabase database) - { - Database = database; - EnsureIndexes(); - } - - protected abstract void CreateIndexes(); - - - static bool _indexesCreated = false; - private void EnsureIndexes() - { - if (!_indexesCreated) - { - - _indexesCreated = true; - } - } - } - -} diff --git a/README.md b/README.md index 71c07ed..cca73ac 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ # NBlockchain +[](https://api.gitsponsors.com/api/badge/link?p=z/lqjMTxNXS+Grw5EvE2h1VgbJs53TSeZowLI8QCrFtKRepan+g1DGIo9DBtV0XQDpvYV9xJb96BKxxNCOYsWQ==) + NBlockchain is a .NET standard library for building blockchain applications. **This project is currently in alpha status and any contributions are welcome.** -The idea is that the developer would only need to focus on the data on rules for a blockchain and not worry about having to build all the infrastructure to facilitate a blockchain. +The idea is that the developer would only need to focus on the data and rules for a blockchain and not worry about having to build all the infrastructure to facilitate a blockchain. The developer would need to * Define the schema of data / transactions they would like to store on the blockchain @@ -18,24 +20,24 @@ Beyond this, it is meant to be highly customizable, you can switch out the defau * Signing * Hashing algorithm * Block verification - * Block notary (eg. proof of work, etc...) + * Block consensus method (eg. proof of work, etc...) ## Installation Using Nuget package console ``` -PM> Install-Package NBlockchain -Version 0.1.0-alpha +PM> Install-Package NBlockchain -Version 0.5.0-alpha ``` Using .NET CLI ``` -dotnet add package NBlockchain --version 0.1.0-alpha +dotnet add package NBlockchain --version 0.5.0-alpha ``` ## Samples * [Digital Currency](Samples/DigitalCurrency) ## Local database stores - * In memory (mostly for testing & demo purposes) + * LiteDB (Default built-in) * [MongoDB](Providers/NBlockchain.MongoDB) ## Networking implementations @@ -47,8 +49,18 @@ dotnet add package NBlockchain --version 0.1.0-alpha * Multicast (for finding peers on the local network) * More to come.... -## Outstanding items for v1 - * Chain fork detection and resolution +## Key features +* Automatic chain fork detection and resolution +* Open, flexible transaction schema +* Customizable transaction level rules +* Customizable block level rules +* Peer discovery +* Proof of work management + +## Documentation +https://github.com/danielgerlag/NBlockchain/tree/master/doc + +## Outstanding items for v1 * NAT traversal * More peer discovery options * Integration tests diff --git a/Samples/DigitalCurrency/DigitalCurrency.csproj b/Samples/DigitalCurrency/DigitalCurrency.csproj index 06957ba..01a527c 100644 --- a/Samples/DigitalCurrency/DigitalCurrency.csproj +++ b/Samples/DigitalCurrency/DigitalCurrency.csproj @@ -8,7 +8,9 @@ + + diff --git a/Samples/DigitalCurrency/Program.cs b/Samples/DigitalCurrency/Program.cs index 260064a..3e45178 100644 --- a/Samples/DigitalCurrency/Program.cs +++ b/Samples/DigitalCurrency/Program.cs @@ -1,4 +1,6 @@ using DigitalCurrency.Repositories; +using DigitalCurrency.Repositories.LiteDb; +using DigitalCurrency.Repositories.Mongo; using DigitalCurrency.Rules; using DigitalCurrency.Transactions; using Microsoft.Extensions.DependencyInjection; @@ -7,39 +9,78 @@ using NBlockchain.Models; using NBlockchain.Services.PeerDiscovery; using System; +using System.Collections.Generic; +using System.Text; namespace DigitalCurrency { class Program { - private static INodeHost _host; - private static IBlockBuilder _miner; + private static IBlockchainNode _host; + private static IBlockMiner _miner; private static IPeerNetwork _network; private static ISignatureService _sigService; private static IAddressEncoder _addressEncoder; - private static ITransactionRepository _txnRepo; + private static ICustomInstructionRepository _txnRepo; + private static IBlockRepository _blockRepo; + private static ITransactionBuilder _txnBuilder; + + static void Main(string[] args) + { + //var serviceProvider = ConfigureForMongoDB("DigitalCurrency", 10500); + var serviceProvider = ConfigureForLiteDb("node.db", 10500); + + _host = serviceProvider.GetService(); + _miner = serviceProvider.GetService(); + _network = serviceProvider.GetService(); + _sigService = serviceProvider.GetService(); + _addressEncoder = serviceProvider.GetService(); + _txnRepo = serviceProvider.GetService(); + _blockRepo = serviceProvider.GetService(); + _txnBuilder = serviceProvider.GetService(); + + Console.Write("Enter passphrase:"); + var phrase = Console.ReadLine(); + var keys = _sigService.GetKeyPairFromPhrase(phrase); + var address = _addressEncoder.EncodeAddress(keys.PublicKey, 0); + Console.WriteLine($"Your address is {address}"); + + _network.Open(); + + PrintHelp(); + while (true) + { + Console.Write(">"); + var command = Console.ReadLine(); + + if (command == "exit") + break; + + RunCommand(command, keys); + } + _network.Close(); + } - private static IServiceProvider ConfigureNode(string db, uint port) + private static IServiceProvider ConfigureForLiteDb(string db, uint port) { IServiceCollection services = new ServiceCollection(); services.AddBlockchain(x => { - x.UseMongoDB(@"mongodb://localhost:27017", db) - .UseTransactionRepository(); + x.UseDataConnection("node.db"); + x.UseInstructionRepository(); x.UseTcpPeerNetwork(port); - //x.AddPeerDiscovery(sp => new StaticPeerDiscovery("tcp://localhost:503")); - x.UseMulticastDiscovery("My Currency", "224.100.0.1", 8088); - x.AddTransactionType(); - x.AddTransactionType(); + x.UseNoNatTraversal(); + x.UseMulticastDiscovery("My Currency", "224.100.0.1", 8088); + x.AddInstructionType(); + x.AddInstructionType(); x.AddTransactionRule(); - x.AddTransactionRule(); + x.AddTransactionRule(); + x.AddBlockRule(); x.UseBlockbaseProvider(); x.UseParameters(new StaticNetworkParameters() { - BlockTime = TimeSpan.FromSeconds(10), - Difficulty = 300, - HeaderVersion = 1, - ExpectedContentThreshold = 0.8m + BlockTime = TimeSpan.FromSeconds(120), + HeaderVersion = 1 }); }); @@ -47,41 +88,47 @@ private static IServiceProvider ConfigureNode(string db, uint port) var serviceProvider = services.BuildServiceProvider(); //config logging - var loggerFactory = serviceProvider.GetService(); + var loggerFactory = serviceProvider.GetService(); + loggerFactory.AddDebug(); + loggerFactory.AddFile("node.log", LogLevel.Debug); return serviceProvider; } - static void Main(string[] args) + private static IServiceProvider ConfigureForMongoDB(string db, uint port) { - var serviceProvider = ConfigureNode("DigitalCurrency", 10500); - - _host = serviceProvider.GetService(); - _miner = serviceProvider.GetService(); - _network = serviceProvider.GetService(); - _sigService = serviceProvider.GetService(); - _addressEncoder = serviceProvider.GetService(); - _txnRepo = serviceProvider.GetService(); - - Console.WriteLine("Generating key pair..."); - var keys = _sigService.GenerateKeyPair(); - var address = _addressEncoder.EncodeAddress(keys.PublicKey, 0); - Console.WriteLine($"Your address is {address}"); - - _network.Open(); - - PrintHelp(); - while (true) + IServiceCollection services = new ServiceCollection(); + services.AddBlockchain(x => { - Console.Write(">"); - var command = Console.ReadLine(); + x.UseTcpPeerNetwork(port); + x.UseUpnpAutoNatTraversal("My Currency"); + x.UseMongoDB(@"mongodb://localhost:27017", db) + .UseInstructionRepository(); + //x.AddPeerDiscovery(sp => new StaticPeerDiscovery("tcp://localhost:503")); + x.UseMulticastDiscovery("My Currency", "224.100.0.1", 8088); + x.UseNoNatTraversal(); + x.AddInstructionType(); + x.AddInstructionType(); + x.AddTransactionRule(); + x.AddTransactionRule(); + x.AddBlockRule(); + x.UseBlockbaseProvider(); + x.UseParameters(new StaticNetworkParameters() + { + BlockTime = TimeSpan.FromSeconds(120), + HeaderVersion = 1 + }); + }); - if (command == "exit") - break; + services.AddLogging(); + var serviceProvider = services.BuildServiceProvider(); - RunCommand(command, keys); - } - _network.Close(); + //config logging + var loggerFactory = serviceProvider.GetService(); + loggerFactory.AddDebug(); + loggerFactory.AddFile("node.log", LogLevel.Debug); + + return serviceProvider; } @@ -104,12 +151,37 @@ static void RunCommand(string command, KeyPair keys) Console.WriteLine("Mining..."); _miner.Start(keys, false); break; + case "stop-mining": + Console.WriteLine("Stopping..."); + _miner.Stop(); + break; + case "peers": + var peersIn = _network.GetPeersIn(); + var peersOut = _network.GetPeersOut(); + Console.WriteLine("Incoming peers"); + PrintPeerList(peersIn); + Console.WriteLine("Outgoing peers"); + PrintPeerList(peersOut); + break; case "balance": if (args.Length == 1) Console.WriteLine($"Balance = {_txnRepo.GetAccountBalance(ownAddress)}"); else Console.WriteLine($"Balance = {_txnRepo.GetAccountBalance(args[1])}"); break; + case "best-block": + var header = _blockRepo.GetBestBlockHeader().Result; + Console.WriteLine($"Height: {header.Height}, Id: {BitConverter.ToString(header.BlockId)}"); + break; + case "gen-key": + var genkeys = _sigService.GetKeyPairFromPhrase(args[1]); + var address = _addressEncoder.EncodeAddress(genkeys.PublicKey, 0); + Console.WriteLine($"{address}"); + break; + case "avg-time": + var avgTime = _blockRepo.GetAverageBlockTimeInSecs(DateTime.UtcNow.AddHours(-1), DateTime.UtcNow).Result; + Console.WriteLine($"Avg time: {avgTime}s"); + break; case "send": if (args.Length != 3) { @@ -117,22 +189,19 @@ static void RunCommand(string command, KeyPair keys) return; } - var txn = new TransferTransaction() + var instruction = new TransferInstruction() { + PublicKey = keys.PublicKey, Amount = Convert.ToInt32(args[2]), - Destination = args[1] + Destination = _addressEncoder.ExtractPublicKeyHash(args[1]) }; - var txnEenv = new TransactionEnvelope(txn) - { - OriginKey = Guid.NewGuid(), - TransactionType = "txn-v1", - Originator = ownAddress - }; - Console.WriteLine($"Singing transaction {txnEenv.OriginKey}"); - _sigService.SignTransaction(txnEenv, keys.PrivateKey); - Console.WriteLine($"Sending transaction {txnEenv.OriginKey}"); - _host.SendTransaction(txnEenv); + Console.WriteLine($"Signing instruction"); + _sigService.SignInstruction(instruction, keys.PrivateKey); + var txn = _txnBuilder.Build(new List() { instruction }).Result; + + Console.WriteLine($"Sending transaction {BitConverter.ToString(txn.TransactionId)}"); + _host.SendTransaction(txn); break; default: Console.WriteLine("invalid command"); @@ -146,11 +215,18 @@ static void PrintHelp() Console.WriteLine("help - prints this message"); Console.WriteLine("mine-genesis - build the genesis block and start mining"); Console.WriteLine("mine - start mining"); + Console.WriteLine("peers - show connected peers"); Console.WriteLine("balance - prints your balance"); Console.WriteLine("balance [address] - prints balance of [address]"); Console.WriteLine("send [address] [amount] - sends [amount] to [address]"); Console.WriteLine("exit - end process"); Console.WriteLine(); } + + static void PrintPeerList(ICollection peers) + { + foreach (var item in peers) + Console.WriteLine($"{item.NodeId} - {item.Address}"); + } } } \ No newline at end of file diff --git a/Samples/DigitalCurrency/Repositories/ITransactionRepository.cs b/Samples/DigitalCurrency/Repositories/ICustomInstructionRepository.cs similarity index 55% rename from Samples/DigitalCurrency/Repositories/ITransactionRepository.cs rename to Samples/DigitalCurrency/Repositories/ICustomInstructionRepository.cs index 1b5edbc..2e86e67 100644 --- a/Samples/DigitalCurrency/Repositories/ITransactionRepository.cs +++ b/Samples/DigitalCurrency/Repositories/ICustomInstructionRepository.cs @@ -4,8 +4,8 @@ namespace DigitalCurrency.Repositories { - public interface ITransactionRepository + public interface ICustomInstructionRepository { - decimal GetAccountBalance(string account); + decimal GetAccountBalance(string address); } } diff --git a/Samples/DigitalCurrency/Repositories/LiteDb/CustomInstructionRepository.cs b/Samples/DigitalCurrency/Repositories/LiteDb/CustomInstructionRepository.cs new file mode 100644 index 0000000..cd95597 --- /dev/null +++ b/Samples/DigitalCurrency/Repositories/LiteDb/CustomInstructionRepository.cs @@ -0,0 +1,43 @@ +using DigitalCurrency.Transactions; +using LiteDB; +using Microsoft.Extensions.Logging; +using NBlockchain.Interfaces; +using NBlockchain.Services.Database; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace DigitalCurrency.Repositories.LiteDb +{ + public class CustomInstructionRepository : InstructionRepository, ICustomInstructionRepository + { + private readonly IAddressEncoder _addressEncoder; + + public CustomInstructionRepository(ILoggerFactory loggerFactory, IDataConnection dataConnection, IAddressEncoder addressEncoder) + : base(loggerFactory, dataConnection) + { + _addressEncoder = addressEncoder; + } + + public decimal GetAccountBalance(string address) + { + var publicKeyHash = _addressEncoder.ExtractPublicKeyHash(address); + + var totalOut = Instructions + .Find(Query.EQ("Statistics.PublicKeyHash", publicKeyHash)) + .Select(x => x.Entity) + .OfType() + .Sum(x => x.Amount); + + + var totalIn = Instructions + .Find(Query.EQ("Entity.Destination", publicKeyHash)) + .Select(x => x.Entity) + .OfType() + .Sum(x => x.Amount); + + return (totalIn - totalOut); + } + } +} diff --git a/Samples/DigitalCurrency/Repositories/Mongo/CustomMongoInstructionRepository.cs b/Samples/DigitalCurrency/Repositories/Mongo/CustomMongoInstructionRepository.cs new file mode 100644 index 0000000..952fe57 --- /dev/null +++ b/Samples/DigitalCurrency/Repositories/Mongo/CustomMongoInstructionRepository.cs @@ -0,0 +1,64 @@ +using MongoDB.Bson; +using MongoDB.Driver; +using NBlockchain.MongoDB.Models; +using NBlockchain.MongoDB.Services; +using System; +using System.Collections.Generic; +using System.Text; +using NBlockchain.Interfaces; + +namespace DigitalCurrency.Repositories.Mongo +{ + public class CustomMongoInstructionRepository : MongoInstructionRepository, ICustomInstructionRepository + { + + private readonly IAddressEncoder _addressEncoder; + + public CustomMongoInstructionRepository(IMongoDatabase database, IAddressEncoder addressEncoder) + : base(database) + { + _addressEncoder = addressEncoder; + } + + protected override void CreateIndexes() + { + } + + public decimal GetAccountBalance(string address) + { + var publicKeyHash = _addressEncoder.ExtractPublicKeyHash(address); + + var totalOut = 0; + var totalIn = 0; + + var outQry = MainChain.Aggregate() + .Unwind(x => x.Transactions) + .Unwind(new StringFieldDefinition("Transactions.Instructions")) + .Match(new BsonDocument("Transactions.Instructions.Statistics.PublicKeyHash", publicKeyHash)) + .Group(new BsonDocument { { "_id", BsonNull.Value }, { "sum", new BsonDocument("$sum", "$Transactions.Instructions.Entity.Amount") } }) + .SingleOrDefault(); + + if (outQry != null) + { + if (outQry.TryGetValue("sum", out var bOut)) + totalOut = bOut.AsInt32; + } + + var inQry = MainChain.Aggregate() + .Unwind(x => x.Transactions) + .Unwind(new StringFieldDefinition("Transactions.Instructions")) + .Match(new BsonDocument("Transactions.Instructions.Entity.Destination", publicKeyHash)) + .Group(new BsonDocument { { "_id", BsonNull.Value }, { "sum", new BsonDocument("$sum", "$Transactions.Instructions.Entity.Amount") } }) + .SingleOrDefault(); + + if (inQry != null) + { + if (inQry.TryGetValue("sum", out var bIn)) + totalIn = bIn.AsInt32; + } + + return (totalIn - totalOut); + } + + } +} diff --git a/Samples/DigitalCurrency/Repositories/TransactionRepository.cs b/Samples/DigitalCurrency/Repositories/TransactionRepository.cs deleted file mode 100644 index cd74011..0000000 --- a/Samples/DigitalCurrency/Repositories/TransactionRepository.cs +++ /dev/null @@ -1,54 +0,0 @@ -using MongoDB.Bson; -using MongoDB.Driver; -using NBlockchain.MongoDB.Services; -using System; -using System.Collections.Generic; -using System.Text; - -namespace DigitalCurrency.Repositories -{ - public class TransactionRepository : MongoTransactionRepository, ITransactionRepository - { - public TransactionRepository(IMongoDatabase database) - : base(database) - { - } - - protected override void CreateIndexes() - { - } - - public decimal GetAccountBalance(string account) - { - var totalOut = 0; - var totalIn = 0; - - var outQry = Blocks.Aggregate() - .Unwind(x => x.Transactions) - .Match(new BsonDocument("Transactions.Originator", account)) - .Group(new BsonDocument { { "_id", BsonNull.Value }, { "sum", new BsonDocument("$sum", "$Transactions.Transaction.Amount") } }) - .SingleOrDefault(); - - if (outQry != null) - { - if (outQry.TryGetValue("sum", out var bOut)) - totalOut = bOut.AsInt32; - } - - var inQry = Blocks.Aggregate() - .Unwind(x => x.Transactions) - .Match(new BsonDocument("Transactions.Transaction.Destination", account)) - .Group(new BsonDocument { { "_id", BsonNull.Value }, { "sum", new BsonDocument("$sum", "$Transactions.Transaction.Amount") } }) - .SingleOrDefault(); - - if (inQry != null) - { - if (inQry.TryGetValue("sum", out var bIn)) - totalIn = bIn.AsInt32; - } - - return (totalIn - totalOut); - } - - } -} diff --git a/Samples/DigitalCurrency/Rules/BalanceRule.cs b/Samples/DigitalCurrency/Rules/BalanceRule.cs index e3eeabe..0c5b6f0 100644 --- a/Samples/DigitalCurrency/Rules/BalanceRule.cs +++ b/Samples/DigitalCurrency/Rules/BalanceRule.cs @@ -4,27 +4,35 @@ using NBlockchain.Services; using System; using System.Collections.Generic; -using System.Text; +using System.Linq; +using NBlockchain.Rules; +using NBlockchain.Interfaces; namespace DigitalCurrency.Rules { - public class BalanceRule : TransactionRule + public class BalanceRule : ITransactionRule { - private readonly ITransactionRepository _txnRepo; + private readonly ICustomInstructionRepository _txnRepo; + private readonly IAddressEncoder _addressEncoder; - public BalanceRule(ITransactionRepository txnRepo) + public BalanceRule(ICustomInstructionRepository txnRepo, IAddressEncoder addressEncoder) { _txnRepo = txnRepo; + _addressEncoder = addressEncoder; } - - protected override int Validate(TransactionEnvelope envelope, TransferTransaction transaction, ICollection siblings) + + public int Validate(Transaction transaction, ICollection siblings) { - if (transaction.Amount < 0) + if (transaction.Instructions.OfType().Any(x => x.Amount < 0)) return 1; - - var balance = _txnRepo.GetAccountBalance(envelope.Originator); - if (transaction.Amount > balance) - return 2; + + foreach (var instruction in transaction.Instructions.OfType()) + { + var sourceAddr = _addressEncoder.EncodeAddress(instruction.PublicKey, 0); + var balance = _txnRepo.GetAccountBalance(sourceAddr); + if (instruction.Amount > balance) + return 2; + } return 0; } diff --git a/Samples/DigitalCurrency/Rules/CoinbaseBlockRule.cs b/Samples/DigitalCurrency/Rules/CoinbaseBlockRule.cs new file mode 100644 index 0000000..bef4f9b --- /dev/null +++ b/Samples/DigitalCurrency/Rules/CoinbaseBlockRule.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using DigitalCurrency.Transactions; +using NBlockchain.Interfaces; +using NBlockchain.Models; + +namespace DigitalCurrency.Rules +{ + class CoinbaseBlockRule : IBlockRule + { + public bool TailRule => false; + + public Task Validate(Block block) + { + var coinbaseCount = block.Transactions.Count(x => x.Instructions.OfType().Any()); + return Task.FromResult(coinbaseCount == 1); + } + } +} diff --git a/Samples/DigitalCurrency/Rules/CoinbaseRule.cs b/Samples/DigitalCurrency/Rules/CoinbaseRule.cs deleted file mode 100644 index 73556f6..0000000 --- a/Samples/DigitalCurrency/Rules/CoinbaseRule.cs +++ /dev/null @@ -1,20 +0,0 @@ -using DigitalCurrency.Transactions; -using NBlockchain.Models; -using NBlockchain.Services; -using System; -using System.Collections.Generic; -using System.Text; - -namespace DigitalCurrency.Rules -{ - public class CoinbaseRule : TransactionRule - { - protected override int Validate(TransactionEnvelope envelope, CoinbaseTransaction transaction, ICollection siblings) - { - if (transaction.Amount != -50) - return 1; - - return 0; - } - } -} diff --git a/Samples/DigitalCurrency/Rules/CoinbaseTransactionRule.cs b/Samples/DigitalCurrency/Rules/CoinbaseTransactionRule.cs new file mode 100644 index 0000000..0de9886 --- /dev/null +++ b/Samples/DigitalCurrency/Rules/CoinbaseTransactionRule.cs @@ -0,0 +1,28 @@ +using DigitalCurrency.Transactions; +using NBlockchain.Models; +using NBlockchain.Services; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NBlockchain.Rules; +using NBlockchain.Interfaces; + +namespace DigitalCurrency.Rules +{ + public class CoinbaseTransactionRule : ITransactionRule + { + public int Validate(Transaction transaction, ICollection siblings) + { + if (transaction.Instructions.OfType().Count() == 0) + return 0; + + var coinbaseTotal = transaction.Instructions.OfType().Sum(x => x.Amount); + + if (coinbaseTotal != -50) + return 1; + + return 0; + } + } +} diff --git a/Samples/DigitalCurrency/Transactions/CoinbaseBuilder.cs b/Samples/DigitalCurrency/Transactions/CoinbaseBuilder.cs index 480478e..a09a0e9 100644 --- a/Samples/DigitalCurrency/Transactions/CoinbaseBuilder.cs +++ b/Samples/DigitalCurrency/Transactions/CoinbaseBuilder.cs @@ -7,19 +7,26 @@ namespace DigitalCurrency.Transactions { - public class CoinbaseBuilder : BlockbaseTransactionBuilder + public class CoinbaseBuilder : BlockbaseTransactionBuilder { - public CoinbaseBuilder(IAddressEncoder addressEncoder, ISignatureService signatureService) - : base(addressEncoder, signatureService) + public CoinbaseBuilder(IAddressEncoder addressEncoder, ISignatureService signatureService, ITransactionBuilder transactionBuilder) + : base(addressEncoder, signatureService, transactionBuilder) { } - protected override CoinbaseTransaction BuildBaseTransaction(ICollection transactions) + protected override ICollection BuildInstructions(KeyPair builderKeys, ICollection transactions) { - return new CoinbaseTransaction() + var result = new List(); + var instruction = new CoinbaseInstruction { - Amount = -50 + Amount = -50, + PublicKey = builderKeys.PublicKey }; + + SignatureService.SignInstruction(instruction, builderKeys.PrivateKey); + result.Add(instruction); + + return result; } } } diff --git a/Samples/DigitalCurrency/Transactions/CoinbaseTransaction.cs b/Samples/DigitalCurrency/Transactions/CoinbaseInstruction.cs similarity index 62% rename from Samples/DigitalCurrency/Transactions/CoinbaseTransaction.cs rename to Samples/DigitalCurrency/Transactions/CoinbaseInstruction.cs index b0fdb45..8984a7f 100644 --- a/Samples/DigitalCurrency/Transactions/CoinbaseTransaction.cs +++ b/Samples/DigitalCurrency/Transactions/CoinbaseInstruction.cs @@ -5,8 +5,8 @@ namespace DigitalCurrency.Transactions { - [TransactionType("coinbase-v1")] - public class CoinbaseTransaction : ValueTransaction + [InstructionType("coinbase-v1")] + public class CoinbaseInstruction : ValueInstruction { } } diff --git a/Samples/DigitalCurrency/Transactions/TransferInstruction.cs b/Samples/DigitalCurrency/Transactions/TransferInstruction.cs new file mode 100644 index 0000000..066cc5a --- /dev/null +++ b/Samples/DigitalCurrency/Transactions/TransferInstruction.cs @@ -0,0 +1,22 @@ +using NBlockchain.Models; +using System; +using System.Collections.Generic; +using System.Text; + +namespace DigitalCurrency.Transactions +{ + [InstructionType("txn-v1")] + public class TransferInstruction : ValueInstruction + { + public string Message { get; set; } + + public byte[] Destination { get; set; } + + public override ICollection ExtractSignableElements() + { + var result = base.ExtractSignableElements(); + result.Add(Destination); + return result; + } + } +} diff --git a/Samples/DigitalCurrency/Transactions/TransferTransaction.cs b/Samples/DigitalCurrency/Transactions/TransferTransaction.cs deleted file mode 100644 index cc21848..0000000 --- a/Samples/DigitalCurrency/Transactions/TransferTransaction.cs +++ /dev/null @@ -1,15 +0,0 @@ -using NBlockchain.Models; -using System; -using System.Collections.Generic; -using System.Text; - -namespace DigitalCurrency.Transactions -{ - [TransactionType("txn-v1")] - public class TransferTransaction : ValueTransaction - { - public string Message { get; set; } - - public string Destination { get; set; } - } -} diff --git a/Samples/DigitalCurrency/Transactions/ValueInstruction.cs b/Samples/DigitalCurrency/Transactions/ValueInstruction.cs new file mode 100644 index 0000000..945a786 --- /dev/null +++ b/Samples/DigitalCurrency/Transactions/ValueInstruction.cs @@ -0,0 +1,17 @@ +using NBlockchain.Models; +using System; +using System.Collections.Generic; +using System.Text; + +namespace DigitalCurrency.Transactions +{ + public abstract class ValueInstruction : Instruction + { + public int Amount { get; set; } + + public override ICollection ExtractSignableElements() + { + return new List() { BitConverter.GetBytes(Amount) }; + } + } +} diff --git a/Samples/DigitalCurrency/Transactions/ValueTransaction.cs b/Samples/DigitalCurrency/Transactions/ValueTransaction.cs deleted file mode 100644 index 5a1a408..0000000 --- a/Samples/DigitalCurrency/Transactions/ValueTransaction.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace DigitalCurrency.Transactions -{ - public abstract class ValueTransaction - { - public int Amount { get; set; } - } -} diff --git a/Samples/DigitalCurrency/readme.md b/Samples/DigitalCurrency/readme.md index 9c598e7..f69bf01 100644 --- a/Samples/DigitalCurrency/readme.md +++ b/Samples/DigitalCurrency/readme.md @@ -1,104 +1,119 @@ # Digital currency sample for NBlockchain -This example demonstrates how to implement a very basic digital currency with NBlockchain +This example demonstrates how to implement a very basic digital currency with NBlockchain. +(This does not follow the flexible input/output locking script scheme that Bitcoin uses but it just meant to illustrate an application of NBlockchain) -## Define our transactions types +## Define our instruction types -The first thing we will do is define the schema of the transactions we want to store in our blockchain. +The first thing we will do is define the schema of the instructions we want to store in our blockchain. +Each block in the blockchain holds a collection of atomic transactions and each transaction is a collection of instructions. ```c# -public abstract class ValueTransaction +public abstract class ValueInstruction : Instruction { public int Amount { get; set; } + + public override ICollection ExtractSignableElements() + { + return new List() { BitConverter.GetBytes(Amount) }; + } } -[TransactionType("txn-v1")] -public class TransferTransaction : ValueTransaction +public class TransferInstruction : ValueInstruction { public string Message { get; set; } - public string Destination { get; set; } + + public byte[] Destination { get; set; } + + public override ICollection ExtractSignableElements() + { + var result = base.ExtractSignableElements(); + result.Add(Destination); + return result; + } } -[TransactionType("coinbase-v1")] -public class CoinbaseTransaction : ValueTransaction +public class CoinbaseInstruction : ValueInstruction { } ``` -In this case we want two types of transactions - * A normal value transfer transaction which is used to send tokens to someone. - * A coinbase transaction that is created per block by the mining node +In this case we want two types of instructions + * A normal value transfer instruction which is used to send tokens to someone. + * A coinbase instruction that is created per block by the mining node -## Implement a repository to query our transactions +## Implement a repository to query our instructions for balance -Now we need to implement a repository to run queries against our defined transactions. -This can be done by extending `MongoTransactionRepository` which gives us access the the block store (if MongoDB is used as the persistence store) +Now we need to implement a repository to run queries against our defined instructions. +This can be done by extending `InstructionRepository` which gives us access the the block store (if MongoDB is used as the persistence store, then you would extend `MongoInstructionRepository`, see [sample](Repositories/Mongo/CustomMongoInstructionRepository.cs)) ```c# -public class TransactionRepository : MongoTransactionRepository, ITransactionRepository +public interface ICustomInstructionRepository { - public TransactionRepository(IMongoDatabase database) - : base(database) - { - } + decimal GetAccountBalance(string address); +} + +public class CustomInstructionRepository : InstructionRepository, ICustomInstructionRepository +{ + private readonly IAddressEncoder _addressEncoder; - public decimal GetAccountBalance(string account) + public CustomInstructionRepository(ILoggerFactory loggerFactory, IDataConnection dataConnection, IAddressEncoder addressEncoder) + : base(loggerFactory, dataConnection) { - var totalOut = 0; - var totalIn = 0; + _addressEncoder = addressEncoder; + } - var outQry = Blocks.Aggregate() - .Unwind(x => x.Transactions) - .Match(new BsonDocument("Transactions.Originator", account)) - .Group(new BsonDocument { { "_id", BsonNull.Value }, { "sum", new BsonDocument("$sum", "$Transactions.Transaction.Amount") } }) - .SingleOrDefault(); + public decimal GetAccountBalance(string address) + { + var publicKeyHash = _addressEncoder.ExtractPublicKeyHash(address); - if (outQry != null) - { - if (outQry.TryGetValue("sum", out var bOut)) - totalOut = bOut.AsInt32; - } + var totalOut = Instructions + .Find(Query.EQ("Statistics.PublicKeyHash", publicKeyHash)) + .Select(x => x.Entity) + .OfType() + .Sum(x => x.Amount); - var inQry = Blocks.Aggregate() - .Unwind(x => x.Transactions) - .Match(new BsonDocument("Transactions.Transaction.Destination", account)) - .Group(new BsonDocument { { "_id", BsonNull.Value }, { "sum", new BsonDocument("$sum", "$Transactions.Transaction.Amount") } }) - .SingleOrDefault(); - if (inQry != null) - { - if (inQry.TryGetValue("sum", out var bIn)) - totalIn = bIn.AsInt32; - } + var totalIn = Instructions + .Find(Query.EQ("Entity.Destination", publicKeyHash)) + .Select(x => x.Entity) + .OfType() + .Sum(x => x.Amount); return (totalIn - totalOut); } - } + ``` -## Define the rules for our transactions +## Define the rules for our instructions -Now we want to define a rule that you cannot spend more than the balance of your wallet. +Now we want to define a rule that you cannot spend more than the balance of your account. ```c# -public class BalanceRule : TransactionRule +public class BalanceRule : ITransactionRule { - private readonly ITransactionRepository _txnRepo; + private readonly ICustomInstructionRepository _txnRepo; + private readonly IAddressEncoder _addressEncoder; - public BalanceRule(ITransactionRepository txnRepo) + public BalanceRule(ICustomInstructionRepository txnRepo, IAddressEncoder addressEncoder) { _txnRepo = txnRepo; + _addressEncoder = addressEncoder; } - - protected override int Validate(TransactionEnvelope envelope, TransferTransaction transaction, ICollection siblings) + + public int Validate(Transaction transaction, ICollection siblings) { - if (transaction.Amount < 0) + if (transaction.Instructions.OfType().Any(x => x.Amount < 0)) return 1; - - var balance = _txnRepo.GetAccountBalance(envelope.Originator); - if (transaction.Amount > balance) - return 2; + + foreach (var instruction in transaction.Instructions.OfType()) + { + var sourceAddr = _addressEncoder.EncodeAddress(instruction.PublicKey, 0); + var balance = _txnRepo.GetAccountBalance(sourceAddr); + if (instruction.Amount > balance) + return 2; + } return 0; } @@ -111,19 +126,23 @@ Now we define how the coinbase transaction is built (by the miners). In this case, we will have a static block reward of 50 ```c# -public class CoinbaseBuilder : BlockbaseTransactionBuilder +public class CoinbaseBuilder : BlockbaseTransactionBuilder { - public CoinbaseBuilder(IAddressEncoder addressEncoder, ISignatureService signatureService) - : base(addressEncoder, signatureService) - { - } + ... - protected override CoinbaseTransaction BuildBaseTransaction(ICollection transactions) + protected override ICollection BuildInstructions(KeyPair builderKeys, ICollection transactions) { - return new CoinbaseTransaction() + var result = new List(); + var instruction = new CoinbaseInstruction { - Amount = -50 + Amount = -50, + PublicKey = builderKeys.PublicKey }; + + SignatureService.SignInstruction(instruction, builderKeys.PrivateKey); + result.Add(instruction); + + return result; } } ``` @@ -132,32 +151,59 @@ public class CoinbaseBuilder : BlockbaseTransactionBuilder When we configure the IoC container for our blockchain node, we have several options In this case we chose - * To use MongoDB as the database + * To use the built-in LiteDb as the database (using node.db as the datafile) + * Register our custome instruction repository that we use in our rule definitions * To use the Tcp peer network and listen on port 500 * Use the multicast peer discovery protocol (to find other peers on the LAN) - * Added our Transaction types that we defined earlier - * Added our Transaction rules - * Set the block time to 10 seconds + * Added our Instruction types that we defined earlier + * Added our Instruction rules + * Added our Block rules + * Set the block time to 120 seconds ```c# IServiceCollection services = new ServiceCollection(); services.AddBlockchain(x => { - x.UseMongoDB(@"mongodb://localhost:27017", "my-currency-db") - .UseTransactionRepository(); - x.UseTcpPeerNetwork(500); + x.UseDataConnection("node.db"); + x.UseInstructionRepository(); + x.UseTcpPeerNetwork(port); x.UseMulticastDiscovery("My Currency", "224.100.0.1", 8088); - x.AddTransactionType(); - x.AddTransactionType(); + x.UseNoNatTraversal(); + x.AddInstructionType(); + x.AddInstructionType(); x.AddTransactionRule(); - x.AddTransactionRule(); + x.AddTransactionRule(); + x.AddBlockRule(); x.UseBlockbaseProvider(); x.UseParameters(new StaticNetworkParameters() { - BlockTime = TimeSpan.FromSeconds(10), - Difficulty = 300, - HeaderVersion = 1, - ExpectedContentThreshold = 0.8m + BlockTime = TimeSpan.FromSeconds(120), + HeaderVersion = 1 }); }); -``` \ No newline at end of file +``` + +If you wanted to use MongoDB as the persistence store, then the config would look something like this + +```c# +IServiceCollection services = new ServiceCollection(); +services.AddBlockchain(x => +{ + x.UseTcpPeerNetwork(port); + x.UseMongoDB(@"mongodb://localhost:27017", db) + .UseInstructionRepository(); + x.UseMulticastDiscovery("My Currency", "224.100.0.1", 8088); + x.UseNoNatTraversal(); + x.AddInstructionType(); + x.AddInstructionType(); + x.AddTransactionRule(); + x.AddTransactionRule(); + x.AddBlockRule(); + x.UseBlockbaseProvider(); + x.UseParameters(new StaticNetworkParameters() + { + BlockTime = TimeSpan.FromSeconds(120), + HeaderVersion = 1 + }); +}); +``` diff --git a/ScratchPad/CustomTransactionRepository.cs b/ScratchPad/CustomTransactionRepository.cs deleted file mode 100644 index 798ecb4..0000000 --- a/ScratchPad/CustomTransactionRepository.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.Extensions.Logging; -using NBlockchain.Interfaces; -using NBlockchain.Models; -using NBlockchain.Services.Database; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace ScratchPad -{ - public class CustomTransactionRepository : TransactionRepository, ICustomTransactionRepository - { - public CustomTransactionRepository(ILoggerFactory loggerFactory, IDataConnection dataConnection) - :base(loggerFactory, dataConnection) - { - } - - public decimal GetAccountBalance(string account) - { - var totalOut = Blocks - .Find(x => x.Entity.Transactions.Count(y => y.Originator == account) > 0) - .SelectMany(x => x.Entity.Transactions) - .Where(x => x.Originator == account) - .Select(x => x.Transaction) - .OfType() - .Sum(x => x.Amount); - - - var totalIn = Blocks.Find(x => x.Entity.Transactions.Select(y => y.Transaction).OfType().Count(y => y.Destination == account) > 0) - .SelectMany(x => x.Entity.Transactions) - .Select(x => x.Transaction) - .OfType() - .Where(x => x.Destination == account) - .Sum(x => x.Amount); - - return (totalIn - totalOut); - } - } -} diff --git a/ScratchPad/ICustomTransactionRepository.cs b/ScratchPad/ICustomTransactionRepository.cs deleted file mode 100644 index bd4a61d..0000000 --- a/ScratchPad/ICustomTransactionRepository.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ScratchPad -{ - public interface ICustomTransactionRepository - { - decimal GetAccountBalance(string account); - } -} \ No newline at end of file diff --git a/ScratchPad/IHashTester.cs b/ScratchPad/IHashTester.cs deleted file mode 100644 index 6bb230f..0000000 --- a/ScratchPad/IHashTester.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace NBlockchain.Services -{ - public interface IHashTester - { - bool TestHash(byte[] hash, uint difficulty); - } -} \ No newline at end of file diff --git a/ScratchPad/Program.cs b/ScratchPad/Program.cs index d209be8..c2ce453 100644 --- a/ScratchPad/Program.cs +++ b/ScratchPad/Program.cs @@ -38,7 +38,7 @@ static void Main(string[] args) var addressEncoder = node1.GetService(); var nodeAddr = addressEncoder.EncodeAddress(node1Keys.PublicKey, 0); Console.WriteLine("sending txn..."); - SendTxn(miner1, keys1, nodeAddr, 15); + SendTxn(miner1, keys1, nodeAddr, 5); }); @@ -55,10 +55,10 @@ static void Main(string[] args) private static KeyPair RunMiner(IServiceProvider sp, bool genesis) { var node = sp.GetService(); - var miner = sp.GetService(); + var miner = sp.GetService(); var network = sp.GetService(); var sigService = sp.GetService(); - var addressEncoder = sp.GetService(); + //var addressEncoder = sp.GetService(); network.Open(); @@ -66,7 +66,7 @@ private static KeyPair RunMiner(IServiceProvider sp, bool genesis) var keys = sigService.GenerateKeyPair(); //var keys2 = sigService.GenerateKeyPair(); - var address = addressEncoder.EncodeAddress(keys.PublicKey, 0); + //var address = addressEncoder.EncodeAddress(keys.PublicKey, 0); miner.Start(keys, genesis); @@ -87,7 +87,7 @@ private static void SendTxn(IServiceProvider sp, KeyPair keys, string address, i Destination = address }; - var txn1env = new TransactionEnvelope(txn1) + var txn1env = new NBlockchain.Models.Transaction(txn1) { OriginKey = Guid.NewGuid(), TransactionType = "txn-v1", @@ -101,7 +101,7 @@ private static void SendTxn(IServiceProvider sp, KeyPair keys, string address, i private static decimal GetBalance(IServiceProvider sp, KeyPair keys) { - var repo = sp.GetService(); + var repo = sp.GetService(); var addressEncoder = sp.GetService(); var address = addressEncoder.EncodeAddress(keys.PublicKey, 0); @@ -129,7 +129,7 @@ private static KeyPair RunNode(IServiceProvider sp) var node = sp.GetService(); var network = sp.GetService(); var sigService = sp.GetService(); - var addressEncoder = sp.GetService(); + //var addressEncoder = sp.GetService(); var keys = sigService.GenerateKeyPair(); network.Open(); @@ -144,10 +144,11 @@ private static IServiceProvider ConfigureNode(string db, uint port, string[] pee services.AddBlockchain(x => { //x.UseMongoDB(@"mongodb://localhost:27017", db) - x.UseTransactionRepository(); + x.UseTransactionRepository(); x.UseTcpPeerNetwork(port); x.AddPeerDiscovery(sp => new StaticPeerDiscovery(peers)); //x.UseMulticastDiscovery("test", "224.100.0.1", 8088); + //x.UseDataConnection($"{db}.db"); x.AddTransactionType(); x.AddTransactionType(); x.AddTransactionRule(); @@ -155,10 +156,8 @@ private static IServiceProvider ConfigureNode(string db, uint port, string[] pee x.UseBlockbaseProvider(); x.UseParameters(new StaticNetworkParameters() { - BlockTime = TimeSpan.FromSeconds(10), - Difficulty = 200, - HeaderVersion = 1, - ExpectedContentThreshold = 0.8m + BlockTime = TimeSpan.FromSeconds(10), + HeaderVersion = 1 }); }); diff --git a/ScratchPad/TestBlockbaseBuilder.cs b/ScratchPad/TestBlockbaseBuilder.cs deleted file mode 100644 index 417b1bc..0000000 --- a/ScratchPad/TestBlockbaseBuilder.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using NBlockchain.Interfaces; -using NBlockchain.Models; -using NBlockchain.Services; - -namespace ScratchPad -{ - public class TestBlockbaseBuilder : BlockbaseTransactionBuilder - { - public TestBlockbaseBuilder(IAddressEncoder addressEncoder, ISignatureService signatureService) - : base(addressEncoder, signatureService) - { - } - - protected override CoinbaseTransaction BuildBaseTransaction(ICollection transactions) - { - return new CoinbaseTransaction() - { - Amount = -50 - }; - } - } -} diff --git a/ScratchPad/TestTransaction.cs b/ScratchPad/TestTransaction.cs deleted file mode 100644 index 85b162a..0000000 --- a/ScratchPad/TestTransaction.cs +++ /dev/null @@ -1,26 +0,0 @@ -using NBlockchain.Models; -using System; -using System.Collections.Generic; -using System.Text; - -namespace ScratchPad -{ - - public class Transaction - { - public int Amount { get; set; } - } - - [TransactionType("txn-v1")] - public class TestTransaction : Transaction - { - public string Message { get; set; } - - public string Destination { get; set; } - } - - [TransactionType("coinbase-v1")] - public class CoinbaseTransaction : Transaction - { - } -} diff --git a/ScratchPad/TestTransactionValidator.cs b/ScratchPad/TestTransactionValidator.cs deleted file mode 100644 index 16e02bb..0000000 --- a/ScratchPad/TestTransactionValidator.cs +++ /dev/null @@ -1,42 +0,0 @@ -using NBlockchain.Models; -using NBlockchain.Services; -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; - -namespace ScratchPad -{ - public class TestTransactionValidator : TransactionRule - { - private readonly ICustomTransactionRepository _txnRepo; - - public TestTransactionValidator(ICustomTransactionRepository txnRepo) - { - _txnRepo = txnRepo; - } - - protected override int Validate(TransactionEnvelope envelope, TestTransaction transaction, ICollection siblings) - { - if (transaction.Amount < 0) - return 1; - - var balance = _txnRepo.GetAccountBalance(envelope.Originator); - if (transaction.Amount > balance) - return 2; - - return 0; - } - } - - public class CoinbaseTransactionValidator : TransactionRule - { - protected override int Validate(TransactionEnvelope envelope, CoinbaseTransaction transaction, ICollection siblings) - { - if (transaction.Amount != -50) - return 1; - - return 0; - } - } -} diff --git a/Tests/NBlockchain.Tests.Scenarios/Common/BaseBuilder.cs b/Tests/NBlockchain.Tests.Scenarios/Common/BaseBuilder.cs index f8fd930..f8a8196 100644 --- a/Tests/NBlockchain.Tests.Scenarios/Common/BaseBuilder.cs +++ b/Tests/NBlockchain.Tests.Scenarios/Common/BaseBuilder.cs @@ -7,16 +7,23 @@ namespace NBlockchain.Tests.Scenarios.Common { - class BaseBuilder : BlockbaseTransactionBuilder + class BaseBuilder : BlockbaseTransactionBuilder { - protected BaseBuilder(IAddressEncoder addressEncoder, ISignatureService signatureService) - : base(addressEncoder, signatureService) + public BaseBuilder(IAddressEncoder addressEncoder, ISignatureService signatureService, ITransactionBuilder transactionBuilder) + : base(addressEncoder, signatureService, transactionBuilder) { } - protected override TestTransaction BuildBaseTransaction(ICollection transactions) + protected override ICollection BuildInstructions(KeyPair builderKeys, ICollection transactions) { - return new TestTransaction() { Data = "base" }; + var instructions = new HashSet(); + var i1 = new TestInstruction(); + i1.Data = "test"; + i1.PublicKey = builderKeys.PublicKey; + SignatureService.SignInstruction(i1, builderKeys.PrivateKey); + instructions.Add(i1); + + return instructions; } } diff --git a/Tests/NBlockchain.Tests.Scenarios/Common/TestTransaction.cs b/Tests/NBlockchain.Tests.Scenarios/Common/TestTransaction.cs index 6da9e17..8539ef0 100644 --- a/Tests/NBlockchain.Tests.Scenarios/Common/TestTransaction.cs +++ b/Tests/NBlockchain.Tests.Scenarios/Common/TestTransaction.cs @@ -5,9 +5,14 @@ namespace NBlockchain.Tests.Scenarios.Common { - [TransactionType("txn-v1")] - public class TestTransaction + [InstructionType("txn-v1")] + public class TestInstruction : Instruction { public string Data { get; set; } + + public override ICollection ExtractSignableElements() + { + return new List() { Encoding.UTF8.GetBytes(Data) }; + } } } diff --git a/Tests/NBlockchain.Tests.Scenarios/NodeSync/NodeOnboardingScenarios.cs b/Tests/NBlockchain.Tests.Scenarios/NodeSync/NodeOnboardingScenarios.cs index 1cb4a1d..5eb23ba 100644 --- a/Tests/NBlockchain.Tests.Scenarios/NodeSync/NodeOnboardingScenarios.cs +++ b/Tests/NBlockchain.Tests.Scenarios/NodeSync/NodeOnboardingScenarios.cs @@ -23,14 +23,12 @@ private static IServiceProvider ConfigureNode(uint port, ICollection pee { x.UseTcpPeerNetwork(port); x.AddPeerDiscovery(sp => new StaticPeerDiscovery(peers)); - x.AddTransactionType(); + x.AddInstructionType(); x.UseBlockbaseProvider(); x.UseParameters(new StaticNetworkParameters() { - BlockTime = TimeSpan.FromSeconds(10), - Difficulty = 200, - HeaderVersion = 1, - ExpectedContentThreshold = 0.8m + BlockTime = TimeSpan.FromSeconds(10), + HeaderVersion = 1 }); }); @@ -82,12 +80,12 @@ public async void should_sync_data_over_mesh() net2.Open(); net3.Open(); - var target = await repo1.GetNewestBlockHeader(); + var target = await repo1.GetBestBlockHeader(); var timeOut = DateTime.Now.AddSeconds(30); while (timeOut > DateTime.Now) { await Task.Delay(500); - var header3 = await repo3.GetNewestBlockHeader(); + var header3 = await repo3.GetBestBlockHeader(); if (header3?.Height == target.Height) break; } @@ -96,8 +94,8 @@ public async void should_sync_data_over_mesh() net2.Close(); net3.Close(); - var last2 = await repo2.GetNewestBlockHeader(); - var last3 = await repo3.GetNewestBlockHeader(); + var last2 = await repo2.GetBestBlockHeader(); + var last3 = await repo3.GetBestBlockHeader(); last2.Height.Should().Be(target.Height); last3.Height.Should().Be(target.Height); diff --git a/Tests/NBlockchain.Tests/Services/BlockchainNodeTests.cs b/Tests/NBlockchain.Tests/Services/BlockchainNodeTests.cs new file mode 100644 index 0000000..25859da --- /dev/null +++ b/Tests/NBlockchain.Tests/Services/BlockchainNodeTests.cs @@ -0,0 +1,576 @@ +using FakeItEasy; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using NBlockchain.Interfaces; +using NBlockchain.Models; +using NBlockchain.Services; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace NBlockchain.Tests.Services +{ + public class BlockchainNodeTests + { + private readonly BlockchainNode _subject; + private readonly IReceiver _receiver; + + private readonly INetworkParameters _parameters; + private readonly IBlockRepository _blockRepository; + private readonly IBlockVerifier _blockVerifier; + private readonly ILoggerFactory _loggerFactory; + private readonly IForkRebaser _forkRebaser; + private readonly IPeerNetwork _peerNetwork; + private readonly IUnconfirmedTransactionPool _unconfirmedTransactionPool; + private readonly IDifficultyCalculator _difficultyCalculator; + + public BlockchainNodeTests() + { + _receiver = A.Fake(); + _parameters = new StaticNetworkParameters() { BlockTime = TimeSpan.FromSeconds(1) }; + _loggerFactory = A.Fake(); + _blockRepository = A.Fake(); + _blockVerifier = A.Fake(); + _forkRebaser = A.Fake(); + _peerNetwork = A.Fake(); + _unconfirmedTransactionPool = A.Fake(); + _difficultyCalculator = A.Fake(); + + _subject = new BlockchainNode(_blockRepository, _blockVerifier, _receiver, _loggerFactory, _forkRebaser, _parameters, _unconfirmedTransactionPool, _peerNetwork, _difficultyCalculator); + _subject.PollTimer.Dispose(); + } + + #region Incoming Transaction + [Fact] + public async void should_verify_incoming_transaction() + { + await _subject.RecieveTransaction(new Transaction() { TransactionId = new byte[0] }); + + A.CallTo(() => _blockVerifier.VerifyTransaction(A.Ignored, A>.Ignored)) + .MustHaveHappened(); + } + + [Fact] + public async void should_ignore_invalid_incoming_transaction() + { + //arrange + A.CallTo(() => _blockVerifier.VerifyTransaction(A.Ignored, A>.Ignored)) + .Returns(-1); + + //act + var result = await _subject.RecieveTransaction(new Transaction() { TransactionId = new byte[0] }); + + //assert + A.CallTo(() => _blockVerifier.VerifyTransaction(A.Ignored, A>.Ignored)) + .MustHaveHappened(); + + A.CallTo(() => _unconfirmedTransactionPool.Add(A.Ignored)) + .MustNotHaveHappened(); + + result.Should().Be(PeerDataResult.Ignore); + } + + [Fact] + public async void should_add_valid_incoming_transaction_to_unconfirmed_pool() + { + //arrange + A.CallTo(() => _blockVerifier.VerifyTransaction(A.Ignored, A>.Ignored)) + .Returns(0); + + A.CallTo(() => _unconfirmedTransactionPool.Add(A.Ignored)) + .Returns(true); + + //act + var result = await _subject.RecieveTransaction(new Transaction() { TransactionId = new byte[0] }); + + //assert + A.CallTo(() => _blockVerifier.VerifyTransaction(A.Ignored, A>.Ignored)) + .MustHaveHappened(); + + A.CallTo(() => _unconfirmedTransactionPool.Add(A.Ignored)) + .MustHaveHappened(); + + result.Should().Be(PeerDataResult.Relay); + } + #endregion + + #region Outgoing Transaction + [Fact] + public async void should_locally_process_outgoing_transaction() + { + //arrange + var txn = new Transaction() { TransactionId = new byte[0] }; + + //act + await _subject.SendTransaction(txn); + + //assert + A.CallTo(() => _receiver.RecieveTransaction(txn)) + .MustHaveHappened(); + } + + [Fact] + public async void should_not_relay_invalid_outgoing_transaction() + { + //arrange + var txn = new Transaction() { TransactionId = new byte[0] }; + A.CallTo(() => _receiver.RecieveTransaction(txn)) + .Returns(PeerDataResult.Ignore); + + //act + await _subject.SendTransaction(txn); + + //assert + A.CallTo(() => _peerNetwork.BroadcastTransaction(txn)) + .MustNotHaveHappened(); + } + + [Fact] + public async void should_relay_valid_outgoing_transaction() + { + //arrange + var txn = new Transaction() { TransactionId = new byte[0] }; + A.CallTo(() => _receiver.RecieveTransaction(txn)) + .Returns(PeerDataResult.Relay); + + //act + await _subject.SendTransaction(txn); + + //assert + A.CallTo(() => _peerNetwork.BroadcastTransaction(txn)) + .MustHaveHappened(); + } + #endregion + + //TODO: complete receive block tests + + #region Incoming Block + + [Fact] + public async void should_ignore_duplicate_blocks() + { + //arrange + var block = BuildBlock(); + A.CallTo(() => _blockRepository.HavePrimaryBlock(block.Header.BlockId)) + .Returns(true); + + A.CallTo(() => _blockRepository.GetBestBlockHeader()) + .Returns(Task.FromResult(null)); + + //act + var result = await _subject.RecieveBlock(block); + + //assert + A.CallTo(() => _blockRepository.HavePrimaryBlock(block.Header.BlockId)) + .MustHaveHappened(); + + A.CallTo(() => _blockRepository.AddBlock(block)) + .MustNotHaveHappened(); + + result.Should().Be(PeerDataResult.Ignore); + } + + [Fact] + public async void should_verify_incoming_block() + { + //arrange + var block = BuildBlock(); + A.CallTo(() => _blockRepository.HavePrimaryBlock(block.Header.BlockId)) + .Returns(false); + + A.CallTo(() => _blockVerifier.Verify(block)) + .Returns(false); + + //act + var result = await _subject.RecieveBlock(block); + + //assert + A.CallTo(() => _blockVerifier.Verify(block)) + .MustHaveHappened(); + + A.CallTo(() => _blockRepository.AddBlock(block)) + .MustNotHaveHappened(); + + result.Should().Be(PeerDataResult.Demerit); + } + + [Fact] + public async void should_execute_block_rules_for_incoming_block() + { + //arrange + var block = BuildBlock(); + A.CallTo(() => _blockRepository.HavePrimaryBlock(block.Header.BlockId)) + .Returns(false); + + A.CallTo(() => _blockVerifier.Verify(block)) + .Returns(true); + + A.CallTo(() => _blockVerifier.VerifyBlockRules(block, A.Ignored)) + .Returns(false); + + //act + var result = await _subject.RecieveBlock(block); + + //assert + A.CallTo(() => _blockVerifier.VerifyBlockRules(block, A.Ignored)) + .MustHaveHappened(); + + A.CallTo(() => _blockRepository.AddBlock(block)) + .MustNotHaveHappened(); + + result.Should().Be(PeerDataResult.Demerit); + } + + [Fact] + public async void should_request_previous_block_if_missing() + { + //arrange + var block = BuildBlock(); + GivenVerificationPasses(block); + + A.CallTo(() => _blockRepository.GetBlockHeader(block.Header.PreviousBlock)) + .Returns(Task.FromResult(null)); + + A.CallTo(() => _blockRepository.GetBestBlockHeader()) + .Returns(Task.FromResult(null)); + + A.CallTo(() => _blockRepository.IsEmpty()) + .Returns(false); + + + //act + var result = await _subject.RecieveBlock(block); + + //assert + A.CallTo(() => _peerNetwork.RequestBlock(block.Header.PreviousBlock)) + .MustHaveHappened(); + + A.CallTo(() => _blockRepository.AddBlock(block)) + .MustNotHaveHappened(); + } + + [Fact] + public async void should_verify_transactions_when_block_is_tip() + { + //arrange + var block = BuildBlock(); + GivenVerificationPasses(block); + GivenBlockIsNextTip(block, block.Header.Height - 1, block.Header.Timestamp - 1, block.Header.Difficulty); + + A.CallTo(() => _blockVerifier.VerifyTransactions(block)) + .Returns(false); + + //act + var result = await _subject.RecieveBlock(block); + + //assert + A.CallTo(() => _blockVerifier.VerifyTransactions(block)) + .MustHaveHappened(); + + A.CallTo(() => _blockRepository.AddBlock(block)) + .MustNotHaveHappened(); + + A.CallTo(() => _unconfirmedTransactionPool.Remove(block.Transactions)) + .MustNotHaveHappened(); + + result.Should().Be(PeerDataResult.Demerit); + } + + [Fact] + public async void should_ignore_block_with_unexpected_height() + { + //arrange + var block = BuildBlock(); + GivenVerificationPasses(block); + GivenBlockIsNextTip(block, block.Header.Height, block.Header.Timestamp - 1, block.Header.Difficulty); + + //act + var result = await _subject.RecieveBlock(block); + + //assert + A.CallTo(() => _blockRepository.AddBlock(block)) + .MustNotHaveHappened(); + + A.CallTo(() => _unconfirmedTransactionPool.Remove(block.Transactions)) + .MustNotHaveHappened(); + + result.Should().Be(PeerDataResult.Ignore); + } + + [Fact] + public async void should_ignore_block_with_unexpected_timestamp() + { + //arrange + var block = BuildBlock(); + GivenVerificationPasses(block); + GivenBlockIsNextTip(block, block.Header.Height - 1, block.Header.Timestamp + 1, block.Header.Difficulty); + + //act + var result = await _subject.RecieveBlock(block); + + //assert + A.CallTo(() => _blockRepository.AddBlock(block)) + .MustNotHaveHappened(); + + A.CallTo(() => _unconfirmedTransactionPool.Remove(block.Transactions)) + .MustNotHaveHappened(); + + result.Should().Be(PeerDataResult.Ignore); + } + + [Fact] + public async void should_ignore_block_with_unexpected_difficulty() + { + //arrange + var block = BuildBlock(); + GivenVerificationPasses(block); + GivenBlockIsNextTip(block, block.Header.Height - 1, block.Header.Timestamp - 1, block.Header.Difficulty - 1); + + //act + var result = await _subject.RecieveBlock(block); + + //assert + A.CallTo(() => _blockRepository.AddBlock(block)) + .MustNotHaveHappened(); + + A.CallTo(() => _unconfirmedTransactionPool.Remove(block.Transactions)) + .MustNotHaveHappened(); + + result.Should().Be(PeerDataResult.Ignore); + } + + [Fact] + public async void should_not_verify_transaction_rules_on_fork_block() + { + //arrange + var block = BuildBlock(); + GivenVerificationPasses(block); + GivenBlockIsFork(block, block.Header.Height - 1, block.Header.Timestamp - 1, block.Header.Difficulty); + + //act + var result = await _subject.RecieveBlock(block); + + //assert + A.CallTo(() => _blockVerifier.VerifyTransactions(block)) + .MustNotHaveHappened(); + + A.CallTo(() => _blockRepository.AddBlock(block)) + .MustNotHaveHappened(); + + A.CallTo(() => _unconfirmedTransactionPool.Remove(block.Transactions)) + .MustNotHaveHappened(); + } + + [Fact] + public async void should_save_block_in_orphan_pool_when_no_parent_in_mainchain() + { + //arrange + var block = BuildBlock(); + GivenVerificationPasses(block); + GivenBlockIsFork(block, block.Header.Height - 1, block.Header.Timestamp - 1, block.Header.Difficulty); + + A.CallTo(() => _blockRepository.HaveSecondaryBlock(block.Header.BlockId)) + .Returns(false); + + //act + var result = await _subject.RecieveBlock(block); + + //assert + A.CallTo(() => _blockRepository.AddSecondaryBlock(block)) + .MustHaveHappened(); + + A.CallTo(() => _blockRepository.AddBlock(block)) + .MustNotHaveHappened(); + + A.CallTo(() => _unconfirmedTransactionPool.Remove(block.Transactions)) + .MustNotHaveHappened(); + } + + [Fact] + public async void should_rebase_chain_when_path_to_better_tip_exists() + { + //arrange + var block = BuildBlock(); + var divergentHeader = new BlockHeader() { BlockId = new byte[] { 0xA } }; + GivenVerificationPasses(block); + GivenBlockIsFork(block, block.Header.Height - 1, block.Header.Timestamp - 1, block.Header.Difficulty); + + A.CallTo(() => _blockRepository.GetDivergentHeader(block.Header.BlockId)) + .Returns(divergentHeader); + + //act + var result = await _subject.RecieveBlock(block); + + //assert + A.CallTo(() => _forkRebaser.RebaseChain(divergentHeader.BlockId, block.Header.BlockId)) + .MustHaveHappened(); + + A.CallTo(() => _blockRepository.AddBlock(block)) + .MustNotHaveHappened(); + + A.CallTo(() => _unconfirmedTransactionPool.Remove(block.Transactions)) + .MustNotHaveHappened(); + } + + [Fact] + public async void should_not_rebase_chain_when_no_path_to_better_tip_exists() + { + //arrange + var block = BuildBlock(); + GivenVerificationPasses(block); + GivenBlockIsFork(block, block.Header.Height - 1, block.Header.Timestamp - 1, block.Header.Difficulty); + + A.CallTo(() => _blockRepository.GetDivergentHeader(block.Header.BlockId)) + .Returns(Task.FromResult(null)); + + //act + var result = await _subject.RecieveBlock(block); + + //assert + A.CallTo(() => _forkRebaser.RebaseChain(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => _blockRepository.AddBlock(block)) + .MustNotHaveHappened(); + + A.CallTo(() => _unconfirmedTransactionPool.Remove(block.Transactions)) + .MustNotHaveHappened(); + } + + [Fact] + public async void should_add_block_if_valid() + { + //arrange + var block = BuildBlock(); + GivenVerificationPasses(block); + GivenBlockIsNextTip(block, block.Header.Height - 1, block.Header.Timestamp - 1, block.Header.Difficulty); + + A.CallTo(() => _blockVerifier.VerifyTransactions(block)) + .Returns(true); + + //act + var result = await _subject.RecieveBlock(block); + + //assert + A.CallTo(() => _blockRepository.AddBlock(block)) + .MustHaveHappened(); + + A.CallTo(() => _unconfirmedTransactionPool.Remove(block.Transactions)) + .MustHaveHappened(); + + result.Should().Be(PeerDataResult.Relay); + } + + #endregion + + #region Setup Helpers + + private Block BuildBlock() + { + return BuildBlock(new byte[] { 0x1, 0x2 }, new byte[] { 0x1, 0x1 }); + } + + private Block BuildBlock(byte[] blockId, byte[] previousBlock) + { + var result = new Block(); + result.Header.BlockId = blockId; + result.Header.PreviousBlock = previousBlock; + result.Header.Height = 100; + + return result; + } + + private void GivenVerificationPasses(Block block) + { + A.CallTo(() => _blockRepository.HavePrimaryBlock(block.Header.BlockId)) + .Returns(false); + + A.CallTo(() => _blockVerifier.Verify(block)) + .Returns(true); + + A.CallTo(() => _blockVerifier.VerifyBlockRules(block, A.Ignored)) + .Returns(true); + } + + private void GivenBlockIsNextTip(Block block, uint prevHeight, long prevTimestamp, uint prevDifficulty) + { + var prevHeader = new BlockHeader() + { + BlockId = block.Header.PreviousBlock, + Height = prevHeight, + Timestamp = prevTimestamp, + Difficulty = prevDifficulty + }; + + A.CallTo(() => _blockRepository.HavePrimaryBlock(block.Header.BlockId)) + .Returns(false); + + A.CallTo(() => _blockRepository.GetBlockHeader(block.Header.PreviousBlock)) + .Returns(prevHeader); + + A.CallTo(() => _blockRepository.GetBestBlockHeader()) + .Returns(prevHeader); + + A.CallTo(() => _blockRepository.IsEmpty()) + .Returns(false); + + A.CallTo(() => _difficultyCalculator.CalculateDifficulty(prevHeader.Timestamp)) + .Returns(prevDifficulty); + + } + + private void GivenBlockIsFork(Block block, uint prevHeight, long prevTimestamp, uint prevDifficulty) + { + var prevHeader = new BlockHeader() + { + BlockId = block.Header.PreviousBlock, + Height = prevHeight, + Timestamp = prevTimestamp, + Difficulty = prevDifficulty + }; + + var mainTip = new BlockHeader() + { + BlockId = new byte[] { 0xFF, 0x2, 0x3 }, + Height = block.Header.Height - 1, + Timestamp = block.Header.Timestamp - 1, + Difficulty = prevDifficulty + }; + + var prevMain = new BlockHeader() + { + BlockId = new byte[] { 0xFF, 0x2, 0x3, 0x4 }, + Height = block.Header.Height - 2, + Timestamp = block.Header.Timestamp - 2, + Difficulty = prevDifficulty + }; + + A.CallTo(() => _blockRepository.HavePrimaryBlock(block.Header.BlockId)) + .Returns(false); + + A.CallTo(() => _blockRepository.GetBlockHeader(block.Header.PreviousBlock)) + .Returns(prevHeader); + + A.CallTo(() => _blockRepository.GetBestBlockHeader()) + .Returns(mainTip); + + A.CallTo(() => _blockRepository.GetPrimaryHeader(block.Header.Height)) + .Returns(mainTip); + + A.CallTo(() => _blockRepository.GetPrimaryHeader(block.Header.Height - 1)) + .Returns(prevMain); + + A.CallTo(() => _blockRepository.IsEmpty()) + .Returns(false); + + A.CallTo(() => _difficultyCalculator.CalculateDifficulty(prevHeader.Timestamp)) + .Returns(prevDifficulty); + + } + + + #endregion + } +} diff --git a/doc/readme.md b/doc/readme.md new file mode 100644 index 0000000..c2ea0d4 --- /dev/null +++ b/doc/readme.md @@ -0,0 +1,43 @@ +# NBlockchain + +**This documentation is still a work in progress!!!** + +NBlockchain is a .NET standard library for building blockchain applications. + +## Block content + +A block consists of a collection of transactions, and transactions in turn are simply a container for instructions. +You may define your own schema for each of your own instruction types by inheriting off the `Instruction` abstract class. + +Once you have defined your own instruction types, you can implement an instruction repository in order to query accepted instructions within your custom rule sets. Do this by inheriting off the `InstructionRepository` class for LiteDb or the `MongoInstructionRepository` for MongoDB. + +You may then define transaction level rules that can inspect the enclosed instructions by implementing the `ITransactionRule` interface. +At this point you may inject your custom instruction repositories into these rule classes via the constructor. + +Furthermore, you may define block level rules that can inspect all the transactions within a block by implementing the `IBlockRule` interface. + +## Node configuration + +You must configure the IoC container with the various pieces you wish to include. eg. + +```c# +services.AddBlockchain(blockchain => +{ + blockchain.UseDataConnection("node.db"); + blockchain.UseInstructionRepository(); + blockchain.UseTcpPeerNetwork(port); + blockchain.UseMulticastDiscovery("My Currency", "224.100.0.1", 8088); + blockchain.UseNoNatTraversal(); + blockchain.AddInstructionType(); + blockchain.AddInstructionType(); + blockchain.AddTransactionRule(); + blockchain.AddTransactionRule(); + blockchain.AddBlockRule(); + blockchain.UseBlockbaseProvider(); + blockchain.UseParameters(new StaticNetworkParameters() + { + BlockTime = TimeSpan.FromSeconds(120), + HeaderVersion = 1 + }); +}); +```