diff --git a/src/Blockcore.sln b/src/Blockcore.sln index 8830248b0..ca76fba55 100644 --- a/src/Blockcore.sln +++ b/src/Blockcore.sln @@ -133,6 +133,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blockcore.Networks.Impleum" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blockcore.Networks.BCP", "Networks\Blockcore.Networks.BCP\Blockcore.Networks.BCP.csproj", "{120500DF-C04F-43CC-9A1E-523A6429301F}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blockcore.Networks.X1", "Networks\Blockcore.Networks.X1\Blockcore.Networks.X1.csproj", "{9A2BA15A-C316-42B4-8E4D-E01B4873190C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -351,6 +353,10 @@ Global {120500DF-C04F-43CC-9A1E-523A6429301F}.Debug|Any CPU.Build.0 = Debug|Any CPU {120500DF-C04F-43CC-9A1E-523A6429301F}.Release|Any CPU.ActiveCfg = Release|Any CPU {120500DF-C04F-43CC-9A1E-523A6429301F}.Release|Any CPU.Build.0 = Release|Any CPU + {9A2BA15A-C316-42B4-8E4D-E01B4873190C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A2BA15A-C316-42B4-8E4D-E01B4873190C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A2BA15A-C316-42B4-8E4D-E01B4873190C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A2BA15A-C316-42B4-8E4D-E01B4873190C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -411,6 +417,7 @@ Global {4275AF0C-587B-4C9D-A100-0F2DD1702674} = {64694A14-97E0-4CBC-8032-754F9353B2DD} {64E9C309-867E-45F6-A88E-7BC061305D0B} = {3B56C02B-4468-4268-B797-851562789FCC} {120500DF-C04F-43CC-9A1E-523A6429301F} = {3B56C02B-4468-4268-B797-851562789FCC} + {9A2BA15A-C316-42B4-8E4D-E01B4873190C} = {3B56C02B-4468-4268-B797-851562789FCC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6C780ABA-5872-4B83-AD3F-A5BD423AD907} diff --git a/src/Networks/Blockcore.Networks.X1/Blockcore.Networks.X1.csproj b/src/Networks/Blockcore.Networks.X1/Blockcore.Networks.X1.csproj new file mode 100644 index 000000000..9c9f3521b --- /dev/null +++ b/src/Networks/Blockcore.Networks.X1/Blockcore.Networks.X1.csproj @@ -0,0 +1,41 @@ + + + + Blockcore.Networks.Xds + Blockcore.Networks.X1 + Blockcore.Networks.X1 + False + true + https://www.blockcore.net + blockchain;cryptocurrency;crypto;C#;.NET;bitcoin;blockcore;x1crypto;x1 + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + \ No newline at end of file diff --git a/src/Networks/Blockcore.Networks.X1/Components/ComponentRegistration.cs b/src/Networks/Blockcore.Networks.X1/Components/ComponentRegistration.cs new file mode 100644 index 000000000..cd64775b7 --- /dev/null +++ b/src/Networks/Blockcore.Networks.X1/Components/ComponentRegistration.cs @@ -0,0 +1,98 @@ +using Blockcore.Base; +using Blockcore.Broadcasters; +using Blockcore.Builder; +using Blockcore.Configuration.Logging; +using Blockcore.Consensus; +using Blockcore.Features.Consensus; +using Blockcore.Features.Consensus.CoinViews; +using Blockcore.Features.Consensus.CoinViews.Coindb; +using Blockcore.Features.Consensus.Interfaces; +using Blockcore.Features.Consensus.ProvenBlockHeaders; +using Blockcore.Features.Consensus.Rules; +using Blockcore.Features.MemoryPool; +using Blockcore.Features.Miner; +using Blockcore.Features.Miner.Broadcasters; +using Blockcore.Features.Miner.Interfaces; +using Blockcore.Features.RPC; +using Blockcore.Features.Wallet.UI; +using Blockcore.Interfaces; +using Blockcore.Interfaces.UI; +using Blockcore.Mining; +using Microsoft.Extensions.DependencyInjection; + +namespace Blockcore.Networks.X1.Components +{ + public static class ComponentRegistration + { + public static IFullNodeBuilder UseX1Consensus(this IFullNodeBuilder fullNodeBuilder) + { + return AddX1PowPosMining(UseX1PosConsensus(fullNodeBuilder)); + } + + static IFullNodeBuilder UseX1PosConsensus(this IFullNodeBuilder fullNodeBuilder) + { + LoggingConfiguration.RegisterFeatureNamespace("posconsensus"); + + fullNodeBuilder.ConfigureFeature(features => + { + features + .AddFeature() + .FeatureServices(services => + { + fullNodeBuilder.PersistenceProviderManager.RequirePersistence(services); + + services.AddSingleton(provider => (IStakdb)provider.GetService()); + services.AddSingleton(); + services.AddSingleton().AddSingleton(provider => provider.GetService()); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton() + .AddSingleton(provider => provider.GetService()) + .AddSingleton(provider => provider.GetService()); + services.AddSingleton(); + + services.AddSingleton(); + }); + }); + + return fullNodeBuilder; + } + + /// + /// Adds POW and POS miner components to the node, so that it can mine or stake. + /// + /// The object used to build the current node. + /// The full node builder, enriched with the new component. + static IFullNodeBuilder AddX1PowPosMining(this IFullNodeBuilder fullNodeBuilder) + { + LoggingConfiguration.RegisterFeatureNamespace("x1mining"); + + fullNodeBuilder.ConfigureFeature(features => + { + features + .AddFeature() + .DependOn() + .DependOn() + // TODO: Need a better way to check dependencies. This is really just dependent on IWalletManager... + // Alternatively "DependsOn" should take a list of features that will satisfy the dependency. + //.DependOn() + .FeatureServices(services => + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + }); + }); + + return fullNodeBuilder; + } + } +} diff --git a/src/Networks/Blockcore.Networks.X1/Components/OpenCLMiner.cs b/src/Networks/Blockcore.Networks.X1/Components/OpenCLMiner.cs new file mode 100644 index 000000000..467294b2d --- /dev/null +++ b/src/Networks/Blockcore.Networks.X1/Components/OpenCLMiner.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Cloo; +using Microsoft.Extensions.Logging; + +namespace Blockcore.Networks.X1.Components +{ + /// + /// The famous SpartaCrypt OpenCLMiner, visit the original here: + /// https://github.com/spartacrypt/xds-1/blob/master/src/components/Fullnode/UnnamedCoin.Bitcoin.Features.Miner/OpenCLMiner.cs + /// + public class OpenCLMiner : IDisposable + { + private const string KernelFunction = "kernel_find_pow"; + + private readonly ILogger logger; + private readonly ComputeDevice computeDevice; + + private List computeKernels = new List(); + private ComputeProgram computeProgram; + private ComputeContext computeContext; + private ComputeKernel computeKernel; + private string[] openCLSources; + private bool isDisposed; + + /// + /// Create a new OpenCLMiner instance. + /// + /// the minerSettings + /// the loggerFactory + public OpenCLMiner(X1MinerSettings minerSettings, ILoggerFactory loggerFactory) + { + this.logger = loggerFactory.CreateLogger(this.GetType().FullName); + var devices = ComputePlatform.Platforms.SelectMany(p => p.Devices).Where(d => d.Available && d.CompilerAvailable).ToList(); + + if (!devices.Any()) + { + this.logger.LogWarning($"No OpenCL Devices Found!"); + } + else + { + foreach (ComputeDevice device in devices) + { + this.logger.LogInformation($"Found OpenCL Device: Name={device.Name}, MaxClockFrequency{device.MaxClockFrequency}"); + } + + this.computeDevice = devices.FirstOrDefault(d => d.Name.Equals(minerSettings.OpenCLDevice, StringComparison.OrdinalIgnoreCase)) ?? devices.FirstOrDefault(); + if (this.computeDevice != null) + { + this.logger.LogInformation($"Using OpenCL Device: Name={this.computeDevice.Name}"); + } + } + } + + /// + /// Destructor. + /// + ~OpenCLMiner() + { + this.DisposeOpenCLResources(); + } + + /// + /// If a compute device for mining is available. + /// + /// true if a usable device is found, otherwise false + public bool CanMine() + { + return this.computeDevice != null; + } + + /// + /// Gets the currently used device name. + /// + /// + public string GetDeviceName() + { + if (this.computeDevice == null) + { + throw new InvalidOperationException("GPU not found"); + } + + return this.computeDevice.Name; + } + + /// + /// Finds the nonce for a block header hash that meets the given target. + /// + /// serialized block header + /// the target + /// the first nonce value to try + /// the number of iterations + /// + public uint FindPow(byte[] header, byte[] bits, uint nonceStart, uint iterations) + { + if (this.computeDevice == null) + { + throw new InvalidOperationException("GPU not found"); + } + + this.ConstructOpenCLResources(); + + using var headerBuffer = new ComputeBuffer(this.computeContext, ComputeMemoryFlags.ReadOnly | ComputeMemoryFlags.CopyHostPointer, header); + using var bitsBuffer = new ComputeBuffer(this.computeContext, ComputeMemoryFlags.ReadOnly | ComputeMemoryFlags.CopyHostPointer, bits); + using var powBuffer = new ComputeBuffer(this.computeContext, ComputeMemoryFlags.WriteOnly, 1); + + this.computeKernel.SetMemoryArgument(0, headerBuffer); + this.computeKernel.SetMemoryArgument(1, bitsBuffer); + this.computeKernel.SetValueArgument(2, nonceStart); + this.computeKernel.SetMemoryArgument(3, powBuffer); + + using var commands = new ComputeCommandQueue(this.computeContext, this.computeDevice, ComputeCommandQueueFlags.None); + commands.Execute(this.computeKernel, null, new long[] { iterations }, null, null); + + var nonceOut = new uint[1]; + commands.ReadFromBuffer(powBuffer, ref nonceOut, true, null); + commands.Finish(); + + this.DisposeOpenCLResources(); + + return nonceOut[0]; + } + + private void ConstructOpenCLResources() + { + if (this.computeDevice != null) + { + if (this.openCLSources == null) + { + GetOpenCLSources(); + } + var properties = new ComputeContextPropertyList(this.computeDevice.Platform); + this.computeContext = new ComputeContext(new[] { this.computeDevice }, properties, null, IntPtr.Zero); + this.computeProgram = new ComputeProgram(this.computeContext, this.openCLSources); + this.computeProgram.Build(new[] { this.computeDevice }, null, null, IntPtr.Zero); + this.computeKernels = this.computeProgram.CreateAllKernels().ToList(); + this.computeKernel = this.computeKernels.First((k) => k.FunctionName == KernelFunction); + } + } + + private void GetOpenCLSources() + { + this.openCLSources = new[] + { + Properties.Resources.SpartacryptOpenCLMiner_opencl_device_info_h, + Properties.Resources.SpartacryptOpenCLMiner_opencl_misc_h, + Properties.Resources.SpartacryptOpenCLMiner_opencl_sha2_common_h, + Properties.Resources.SpartacryptOpenCLMiner_opencl_sha512_h, + Properties.Resources.SpartacryptOpenCLMiner_sha512_miner_cl + }; + } + + private void DisposeOpenCLResources() + { + this.computeKernels.ForEach(k => k.Dispose()); + this.computeKernels.Clear(); + this.computeProgram?.Dispose(); + this.computeProgram = null; + this.computeContext?.Dispose(); + this.computeContext = null; + } + + /// + /// Releases the OpenCL resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + void Dispose(bool disposing) + { + if (this.isDisposed) + return; + + if (disposing) + { + DisposeOpenCLResources(); + } + + this.isDisposed = true; + } + } +} diff --git a/src/Networks/Blockcore.Networks.X1/Components/X1MinerSettings.cs b/src/Networks/Blockcore.Networks.X1/Components/X1MinerSettings.cs new file mode 100644 index 000000000..924799d4c --- /dev/null +++ b/src/Networks/Blockcore.Networks.X1/Components/X1MinerSettings.cs @@ -0,0 +1,141 @@ +using System.Text; +using Blockcore.Configuration; +using Blockcore.Features.Miner; +using Blockcore.Utilities; +using Microsoft.Extensions.Logging; +using NBitcoin; + +namespace Blockcore.Networks.X1.Components +{ + /// + /// Configuration related to the miner interface. + /// + public class X1MinerSettings : MinerSettings + { + private const ulong MinimumSplitCoinValueDefaultValue = 100 * Money.COIN; + + private const ulong MinimumStakingCoinValueDefaultValue = 10 * Money.CENT; + + /// Instance logger. + private readonly ILogger logger; + + /// + /// Set the threads for CPU mining. + /// + public int MineThreadCount { get; } + + /// + /// Use a GPU to mine if available, Default true. + /// + public bool UseOpenCL { get; } + + /// + /// The name of the OpenCLDevice to use. Default is first one found. + /// + public string OpenCLDevice { get; set; } + + /// + /// Amount to split the work to send to the OpenCL device. + /// Experiment with this value to find the optimum between a short execution time and big hash rate. + /// + public int OpenCLWorksizeSplit { get; } + + + + /// + /// Initializes an instance of the object from the node configuration. + /// + /// The node configuration. + public X1MinerSettings(NodeSettings nodeSettings) : base(nodeSettings) + { + Guard.NotNull(nodeSettings, nameof(nodeSettings)); + + this.logger = nodeSettings.LoggerFactory.CreateLogger(typeof(X1MinerSettings).FullName); + + TextFileConfiguration config = nodeSettings.ConfigReader; + + if (this.Mine) + { + this.MineThreadCount = config.GetOrDefault("minethreads",1, this.logger); + this.UseOpenCL = config.GetOrDefault("useopencl", false, this.logger); + this.OpenCLDevice = config.GetOrDefault("opencldevice", string.Empty, this.logger); + this.OpenCLWorksizeSplit = config.GetOrDefault("openclworksizesplit", 10, this.logger); + } + } + + /// + /// Displays mining help information on the console. + /// + /// Not used. + public new static void PrintHelp(Network network) + { + NodeSettings defaults = NodeSettings.Default(network); + var builder = new StringBuilder(); + + builder.AppendLine("-mine=<0 or 1> Enable POW mining."); + builder.AppendLine("-mineaddress= The address to use for mining (empty string to select an address from the wallet)."); + builder.AppendLine("-minethreads=1 Total threads to mine on (default 1)."); + builder.AppendLine("-useopencl=<0 or 1> Use OpenCL for POW mining (default 0)"); + builder.AppendLine("-opencldevice= Name of the OpenCL device to use (default first available)."); + builder.AppendLine("-openclworksizesplit= Default 10. Amount to split the work to send to the OpenCL device. Experiment with this value to find the optimum between a short execution time and big hash rate."); + + builder.AppendLine("-stake=<0 or 1> Enable POS."); + builder.AppendLine("-mineaddress= The address to use for mining (empty string to select an address from the wallet)."); + builder.AppendLine("-walletname= The wallet name to use when staking."); + builder.AppendLine("-walletpassword= Password to unlock the wallet."); + builder.AppendLine("-blockmaxsize= Maximum block size (in bytes) for the miner to generate."); + builder.AppendLine("-blockmaxweight= Maximum block weight (in weight units) for the miner to generate."); + builder.AppendLine("-blockmintxfee= Minimum fee rate for transactions to be included in blocks created by miner."); + builder.AppendLine("-enablecoinstakesplitting=<0 or 1> Enable splitting coins when staking. This is true by default."); + builder.AppendLine($"-minimumstakingcoinvalue= Minimum size of the coins considered for staking, in satoshis. Default value is {MinimumStakingCoinValueDefaultValue:N0} satoshis (= {MinimumStakingCoinValueDefaultValue / (decimal)Money.COIN:N1} Coin)."); + builder.AppendLine($"-minimumsplitcoinvalue= Targeted minimum value of staking coins after splitting, in satoshis. Default value is {MinimumSplitCoinValueDefaultValue:N0} satoshis (= {MinimumSplitCoinValueDefaultValue / Money.COIN} Coin)."); + + builder.AppendLine($"-enforceStakingFlag=<0 or 1> If true staking will require whitelisting addresses in order to stake. Defult is false"); + + defaults.Logger.LogInformation(builder.ToString()); + } + + /// + /// Get the default configuration. + /// + /// The string builder to add the settings to. + /// The network to base the defaults off. + public new static void BuildDefaultConfigurationFile(StringBuilder builder, Network network) + { + builder.AppendLine("####Miner Settings####"); + builder.AppendLine("#Enable POW mining."); + builder.AppendLine("#mine=0"); + builder.AppendLine("#Enable POS."); + builder.AppendLine("#stake=0"); + builder.AppendLine("#The address to use for mining (empty string to select an address from the wallet)."); + builder.AppendLine("#mineaddress="); + builder.AppendLine("#Total threads to mine on (default 1).."); + builder.AppendLine("#minethreads=1"); + builder.AppendLine("#Use OpenCL for POW mining."); + builder.AppendLine("#useopencl=0"); + builder.AppendLine("#Name of the OpenCL device to use (defaults to first available)."); + builder.AppendLine("#opencldevice="); + builder.AppendLine("#Amount to split the work to send to the OpenCL device. Experiment with this value to find the optimum between a short execution time and big hash rate."); + builder.AppendLine("#openclworksizesplit=10"); + builder.AppendLine("#The wallet name to use when staking."); + builder.AppendLine("#walletname="); + builder.AppendLine("#Password to unlock the wallet."); + builder.AppendLine("#walletpassword="); + builder.AppendLine("#Maximum block size (in bytes) for the miner to generate."); + builder.AppendLine($"#blockmaxsize={network.Consensus.Options.MaxBlockSerializedSize}"); + builder.AppendLine("#Maximum block weight (in weight units) for the miner to generate."); + builder.AppendLine($"#blockmaxweight={network.Consensus.Options.MaxBlockWeight}"); + builder.AppendLine("#Minimum fee rate for transactions to be included in blocks created by miner."); + builder.AppendLine($"#blockmintxfee={network.Consensus.Options.MinBlockFeeRate}"); + builder.AppendLine("#Enable splitting coins when staking."); + builder.AppendLine("#enablecoinstakesplitting=1"); + builder.AppendLine("#Minimum size of the coins considered for staking, in satoshis."); + builder.AppendLine($"#minimumstakingcoinvalue={MinimumStakingCoinValueDefaultValue}"); + builder.AppendLine("#Targeted minimum value of staking coins after splitting, in satoshis."); + builder.AppendLine($"#minimumsplitcoinvalue={MinimumSplitCoinValueDefaultValue}"); + builder.AppendLine("#If staking will require whitelisting addresses in order to stake. Defult is false."); + builder.AppendLine($"#enforceStakingFlag=0"); + + } + } +} diff --git a/src/Networks/Blockcore.Networks.X1/Components/X1MiningFeature.cs b/src/Networks/Blockcore.Networks.X1/Components/X1MiningFeature.cs new file mode 100644 index 000000000..6ac1fff74 --- /dev/null +++ b/src/Networks/Blockcore.Networks.X1/Components/X1MiningFeature.cs @@ -0,0 +1,209 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Blockcore.Base; +using Blockcore.Broadcasters; +using Blockcore.Builder; +using Blockcore.Builder.Feature; +using Blockcore.Configuration; +using Blockcore.Configuration.Logging; +using Blockcore.Configuration.Settings; +using Blockcore.Consensus.ScriptInfo; +using Blockcore.Features.BlockStore; +using Blockcore.Features.MemoryPool; +using Blockcore.Features.Miner; +using Blockcore.Features.Miner.Broadcasters; +using Blockcore.Features.Miner.Interfaces; +using Blockcore.Features.Miner.Staking; +using Blockcore.Features.RPC; +using Blockcore.Features.Wallet; +using Blockcore.Features.Wallet.UI; +using Blockcore.Interfaces.UI; +using Blockcore.Mining; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NBitcoin; +using NBitcoin.DataEncoders; + +namespace Blockcore.Networks.X1.Components +{ + /// + /// Provides an ability to mine or stake. + /// + public class X1MiningFeature : FullNodeFeature + { + private readonly ConnectionManagerSettings connectionManagerSettings; + + /// Specification of the network the node runs on - regtest/testnet/mainnet. + private readonly Network network; + + /// Settings relevant to mining or staking. + private readonly X1MinerSettings minerSettings; + + /// Settings relevant to node. + private readonly NodeSettings nodeSettings; + + /// POW miner. + private readonly IPowMining powMining; + + /// POS staker. + private readonly IPosMinting posMinting; + + /// Instance logger. + private readonly ILogger logger; + + /// State of time synchronization feature that stores collected data samples. + private readonly ITimeSyncBehaviorState timeSyncBehaviorState; + + public X1MiningFeature( + ConnectionManagerSettings connectionManagerSettings, + Network network, + MinerSettings minerSettings, + NodeSettings nodeSettings, + ILoggerFactory loggerFactory, + ITimeSyncBehaviorState timeSyncBehaviorState, + IPowMining powMining, + IPosMinting posMinting = null) + { + this.connectionManagerSettings = connectionManagerSettings; + this.network = network; + this.minerSettings = (X1MinerSettings)minerSettings; + this.nodeSettings = nodeSettings; + this.powMining = powMining; + this.timeSyncBehaviorState = timeSyncBehaviorState; + this.posMinting = posMinting; + this.logger = loggerFactory.CreateLogger(this.GetType().FullName); + } + + /// + /// Prints command-line help. + /// + /// The network to extract values from. + public static void PrintHelp(Network network) + { + X1MinerSettings.PrintHelp(network); + } + + /// + /// Get the default configuration. + /// + /// The string builder to add the settings to. + /// The network to base the defaults off. + public static void BuildDefaultConfigurationFile(StringBuilder builder, Network network) + { + X1MinerSettings.BuildDefaultConfigurationFile(builder, network); + } + + /// + /// Starts staking a wallet. + /// + /// The name of the wallet. + /// The password of the wallet. + public void StartStaking(string walletName, string walletPassword) + { + if (this.timeSyncBehaviorState.IsSystemTimeOutOfSync) + { + string errorMessage = "Staking cannot start, your system time does not match that of other nodes on the network." + Environment.NewLine + + "Please adjust your system time and restart the node."; + this.logger.LogError(errorMessage); + throw new ConfigurationException(errorMessage); + } + + if (!string.IsNullOrEmpty(walletName) && !string.IsNullOrEmpty(walletPassword)) + { + this.logger.LogInformation("Staking enabled on wallet '{0}'.", walletName); + + this.posMinting.Stake(new WalletSecret + { + WalletPassword = walletPassword, + WalletName = walletName + }); + } + else + { + string errorMessage = "Staking not started, wallet name or password were not provided."; + this.logger.LogError(errorMessage); + throw new ConfigurationException(errorMessage); + } + } + + /// + /// Stop a staking wallet. + /// + public void StopStaking() + { + this.posMinting?.StopStake(); + this.logger.LogInformation("Staking stopped."); + } + + /// + /// Stop a Proof of Work miner. + /// + public void StopMining() + { + this.powMining?.StopMining(); + this.logger.LogInformation("Mining stopped."); + } + + /// + public override Task InitializeAsync() + { + if (this.minerSettings.Mine) + { + this.powMining.Mine(GetMineToAddress(this.minerSettings.MineAddress)); + this.logger.LogInformation("X1 Mining enabled."); + } + + if (this.minerSettings.Stake) + { + this.StartStaking(this.minerSettings.WalletName, this.minerSettings.WalletPassword); + } + + return Task.CompletedTask; + } + + Script GetMineToAddress(string address) + { + try + { + byte[] bytes = this.network.Bech32Encoders.First().Decode(address, out byte witVersion); + + if (witVersion == 0 && bytes.Length == 20) + return new BitcoinWitPubKeyAddress(address, this.network).ScriptPubKey; + if (witVersion == 0 && bytes.Length == 32) + return new BitcoinWitScriptAddress(address, this.network).ScriptPubKey; + throw new InvalidOperationException("Invalid value for 'mineaddress' in .config or parameters."); + } + catch (Exception e) + { + throw new InvalidOperationException("Mining is enabled but misconfigured.", e); + } + } + + /// + public override void Dispose() + { + this.StopMining(); + this.StopStaking(); + } + + /// + public override void ValidateDependencies(IFullNodeServiceProvider services) + { + if (services.ServiceProvider.GetService() != null) + { + services.Features.EnsureFeature(); + } + + // Mining and staking require block store feature. + if (this.minerSettings.Mine || this.minerSettings.Stake) + { + services.Features.EnsureFeature(); + var storeSettings = services.ServiceProvider.GetService(); + if (storeSettings.PruningEnabled) + throw new ConfigurationException("BlockStore prune mode is incompatible with mining and staking."); + } + } + } +} \ No newline at end of file diff --git a/src/Networks/Blockcore.Networks.X1/Components/X1PosMinting.cs b/src/Networks/Blockcore.Networks.X1/Components/X1PosMinting.cs new file mode 100644 index 000000000..b7c53da35 --- /dev/null +++ b/src/Networks/Blockcore.Networks.X1/Components/X1PosMinting.cs @@ -0,0 +1,1251 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Blockcore.AsyncWork; +using Blockcore.Base; +using Blockcore.Consensus; +using Blockcore.Consensus.BlockInfo; +using Blockcore.Consensus.Chain; +using Blockcore.Consensus.ScriptInfo; +using Blockcore.Consensus.TransactionInfo; +using Blockcore.Features.Consensus; +using Blockcore.Features.Consensus.CoinViews; +using Blockcore.Features.Consensus.Interfaces; +using Blockcore.Features.Consensus.Rules.CommonRules; +using Blockcore.Features.Consensus.Rules.UtxosetRules; +using Blockcore.Features.MemoryPool; +using Blockcore.Features.MemoryPool.Interfaces; +using Blockcore.Features.Miner; +using Blockcore.Features.Miner.Api.Models; +using Blockcore.Features.Miner.Interfaces; +using Blockcore.Features.Miner.Staking; +using Blockcore.Features.Wallet; +using Blockcore.Features.Wallet.Interfaces; +using Blockcore.Features.Wallet.Types; +using Blockcore.Interfaces; +using Blockcore.Mining; +using Blockcore.Networks; +using Blockcore.Networks.X1.Consensus; +using Blockcore.Utilities; +using Microsoft.Extensions.Logging; +using NBitcoin; +using NBitcoin.BuilderExtensions; +using NBitcoin.Crypto; +using NBitcoin.Protocol; + +namespace Blockcore.Networks.X1.Components +{ + /// + /// is used in order to generate new blocks. It involves a sort of lottery, similar to proof-of-work, + /// but the chances of winning this lottery is proportional to how many coins you are staking, not on hashing power. + /// + /// + /// Staking is attempted only if the node is fully synchronized and connected to the network. + /// If not it will wait till node is synced. Only transactions that were confirmed at least + /// blocks ago are eligible for staking. + /// + /// The overall process for "attempting" to mine a PoS block looks like this: + /// + /// Create new block with transactions from mempool. + /// Get UTXOs that can participate in staking (have suitable depth). + /// Split these UTXOs in subsets and create tasks processing each subset to allow for parallel processing. + /// Each of the tasks mentioned above will try to find a solution for proof of stake target. This is done by creating a coinstake + /// transaction with each of the available UTXOs combined with all valid unix timestamps that were not checked. + /// Those timestamps are within a time interval from now to now - searchInterval seconds. Only timestamps that are divisible by + /// + 1 are valid candidates (this is done to decrease granularity of timestamps). + /// Search interval is a length of an unexplored block time space in seconds. + /// Task calculates the kernel's hash (kernel is the first input in the coinstake transaction) using the next formula: + /// hash(stakeModifierV2 + stakingCoins.Time + prevout.Hash + prevout.N + transactionTime). + /// Then it calculates staking target using the next formula: block difficulty * UTXO value. + /// We compare kernel's hash against the staking target, if it's greater then we met the criteria and kernel is found. + /// So the more coins we stake the higher the staking target and so the higher the chance to meet the criteria. + /// In case kernel is found we add a coinstake transaction, sign the block and add it to the chain. + /// + /// + /// + /// Coinstake transaction invalidates previous inputs and spends the inputs to new outputs with the additional stake reward. + /// + /// + /// The purpose of stake modifier is to prevent a UTXO owner from computing future proof-of-stake + /// generated by this UTXO at the time of transaction confirmation. As described above, the stake modifier + /// is included in the hash that must meet the difficulty target. As the stake modifier changes with each block + /// and the new value depends on the kernel, it is hard to predict its value in the future. + /// + /// + public class X1PosMinting : IPosMinting + { + /// + /// Indicates the current state: idle, staking requested, staking in progress and stop staking requested. + /// + public enum CurrentState + { + Idle = 0, + StakingRequested = 1, + StakingInProgress = 2, + StopStakingRequested = 3 + } + + /// The maximum allowed size for a serialized block, in bytes (network rule). + public const int MaxBlockSize = 1000000; + + /// The maximum size for mined blocks. + public const int MaxBlockSizeGen = MaxBlockSize / 2; + + /// Builder that creates a proof-of-stake block template. + private readonly IBlockProvider blockProvider; + + /// true if coinstake transaction splits the coin and generates extra UTXO + /// to prevent halting chain; false to disable coinstake splitting. + public readonly bool CoinstakeSplitEnabled; + + /// If is set, the coinstake will be split if + /// the number of non-empty UTXOs in the wallet is lower than the required coin age for staking plus 1, + /// multiplied by this value. See . + public const int CoinstakeSplitLimitMultiplier = 3; + + /// Number of UTXO descriptions that a single worker's task will process. + /// To achieve a good level of parallelism, this should be low enough so that CPU threads are used, + /// but high enough to compensate for tasks' overhead. + private const int UtxoStakeDescriptionsPerCoinstakeWorker = 25; + + /// Consensus manager class. + private readonly IConsensusManager consensusManager; + + /// Thread safe access to the best chain of block headers (that the node is aware of) from genesis. + private readonly ChainIndexer chainIndexer; + + /// Specification of the network the node runs on - regtest/testnet/mainnet. + private readonly Network network; + + /// Provides date time functionality. + private readonly IDateTimeProvider dateTimeProvider; + + /// Global application life cycle control - triggers when application shuts down. + private readonly INodeLifetime nodeLifetime; + + /// Consensus' view of UTXO set. + private readonly ICoinView coinView; + + /// Database of stake related data for the current blockchain. + private readonly IStakeChain stakeChain; + + /// Provides functionality for checking validity of PoS blocks. + private readonly IStakeValidator stakeValidator; + + /// Factory for creating background async loop tasks. + private readonly IAsyncProvider asyncProvider; + + /// A manager providing operations on wallets. + private readonly IWalletManager walletManager; + + /// Factory for creating loggers. + private readonly ILoggerFactory loggerFactory; + + private readonly X1MinerSettings minerSettings; + + /// Instance logger. + private readonly ILogger logger; + + /// Loop in which the node attempts to generate new POS blocks by staking coins from its wallet. + private IAsyncLoop stakingLoop; + + /// A flag that indicates the current state based on the enum. + private int currentState; + + /// + /// We don't stake coins that are smaller than 0.1 in order to save on CPU as these have a very small chance to be used + /// to generate a block anyway. + /// + /// + public readonly ulong MinimumStakingCoinValue; + + /// When splitting a big utxo, this is the number of smaller utxos we divide it into. + internal const int SplitFactor = 8; + + /// Minimum value of a split utxo we are aiming for (after splitting it into equal parts). + private readonly ulong MinimumSplitCoinValue; + + /// + /// Target reserved balance that will not participate in staking. + /// It is possible that less than this amount will be reserved. + /// + private Money targetReserveBalance; + + /// Time in milliseconds between attempts to generate PoS blocks. + private readonly int minerSleep; + + /// Time in milliseconds between attempts to generate PoS blocks, when the system time is out of sync. + private readonly int systemTimeOutOfSyncSleep; + + /// A lock for managing asynchronous access to memory pool. + protected readonly MempoolSchedulerLock mempoolLock; + + /// Memory pool of pending transactions. + protected readonly ITxMempool mempool; + + /// Script types that can participate in staking. + public Dictionary ValidStakingTemplates; + + /// Information about node's staking for RPC "getstakinginfo" command. + /// This object does not need a synchronized access because there is no execution logic + /// that depends on the reported information. + private GetStakingInfoModel rpcGetStakingInfoModel; + + /// Estimation of the total staking weight of all nodes on the network. + private long networkWeight; + + /// + /// Timestamp of the last attempt to search for POS solution. + /// + /// It is used to prevent searching for solutions that were already proved wrong in the past. + /// If there is no new block since last time we searched for the solution, it does not make + /// sense to try timestamps earlier than this value. + /// + /// + private long lastCoinStakeSearchTime; + + /// + /// Hash of the block headers of the block that was at the tip of the chain during our last + /// search for POS solution. + /// + /// It is used to prevent searching for solutions that were already proved wrong in the past. + /// If the tip changes, is set to the new tip's header hash. + /// + /// + private uint256 lastCoinStakeSearchPrevBlockHash; + + /// + /// A cancellation token source that can cancel the staking processes and is linked to the . + /// + private CancellationTokenSource stakeCancellationTokenSource; + + /// Provider of IBD state. + private readonly IInitialBlockDownloadState initialBlockDownloadState; + + /// State of time synchronization feature that stores collected data samples. + private readonly ITimeSyncBehaviorState timeSyncBehaviorState; + + public X1PosMinting( + IBlockProvider blockProvider, + IConsensusManager consensusManager, + ChainIndexer chainIndexer, + Network network, + IDateTimeProvider dateTimeProvider, + IInitialBlockDownloadState initialBlockDownloadState, + INodeLifetime nodeLifetime, + ICoinView coinView, + IStakeChain stakeChain, + IStakeValidator stakeValidator, + MempoolSchedulerLock mempoolLock, + ITxMempool mempool, + IWalletManager walletManager, + IAsyncProvider asyncProvider, + ITimeSyncBehaviorState timeSyncBehaviorState, + ILoggerFactory loggerFactory, + MinerSettings minerSettings) + { + this.blockProvider = blockProvider; + this.consensusManager = consensusManager; + this.chainIndexer = chainIndexer; + this.network = network; + this.dateTimeProvider = dateTimeProvider; + this.initialBlockDownloadState = initialBlockDownloadState; + this.nodeLifetime = nodeLifetime; + this.coinView = coinView; + this.stakeChain = stakeChain; + this.stakeValidator = stakeValidator; + this.mempoolLock = mempoolLock; + this.mempool = mempool; + this.asyncProvider = asyncProvider; + this.walletManager = walletManager; + this.timeSyncBehaviorState = timeSyncBehaviorState; + this.loggerFactory = loggerFactory; + this.minerSettings = (X1MinerSettings)minerSettings; + this.logger = loggerFactory.CreateLogger(this.GetType().FullName); + + this.minerSleep = 500; // GetArg("-minersleep", 500); + this.systemTimeOutOfSyncSleep = 7000; + this.lastCoinStakeSearchTime = this.dateTimeProvider.GetAdjustedTimeAsUnixTimestamp(); + this.lastCoinStakeSearchPrevBlockHash = 0; + this.targetReserveBalance = 0; // TODO:settings.targetReserveBalance + this.currentState = (int)CurrentState.Idle; + + this.rpcGetStakingInfoModel = new GetStakingInfoModel(); + + this.CoinstakeSplitEnabled = minerSettings.EnableCoinStakeSplitting; + this.MinimumStakingCoinValue = minerSettings.MinimumStakingCoinValue; + this.MinimumSplitCoinValue = minerSettings.MinimumSplitCoinValue; + this.ValidStakingTemplates = walletManager.GetValidStakingTemplates(); + } + + /// + public void Stake(WalletSecret walletSecret) + { + Guard.NotNull(walletSecret, nameof(walletSecret)); + + if (Interlocked.CompareExchange(ref this.currentState, (int)CurrentState.StakingRequested, (int)CurrentState.Idle) != (int)CurrentState.Idle) + { + this.logger.LogTrace("(-)[NOT_IDLE]"); + return; + } + + this.rpcGetStakingInfoModel.Enabled = true; + this.stakeCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(new[] { this.nodeLifetime.ApplicationStopping }); + + this.stakingLoop = this.asyncProvider.CreateAndRunAsyncLoop("PosMining.Stake", async token => + { + try + { + await this.GenerateBlocksAsync(walletSecret, token) + .ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Application stopping, nothing to do as the loop will be stopped. + } + catch (MinerException me) + { + // Miner exceptions should be ignored. It means that the miner + // possibly mined a block that was not accepted by peers or is even invalid, + // but it should not halted the staking operation. + this.logger.LogDebug("Miner exception occurred in miner loop: {0}", me.ToString()); + this.rpcGetStakingInfoModel.Errors = me.Message; + } + catch (ConsensusErrorException cee) + { + // All consensus exceptions should be ignored. It means that the miner + // run into problems while constructing block or verifying it + // but it should not halted the staking operation. + this.logger.LogDebug("Consensus error exception occurred in miner loop: {0}", cee.ToString()); + this.rpcGetStakingInfoModel.Errors = cee.Message; + } + catch (ConsensusException ce) + { + // All consensus exceptions should be ignored. It means that the miner + // run into problems while constructing block or verifying it + // but it should not halted the staking operation. + this.logger.LogDebug("Consensus exception occurred in miner loop: {0}", ce.ToString()); + this.rpcGetStakingInfoModel.Errors = ce.Message; + } + catch (Exception ex) + { + this.logger.LogError("Exception: {0}", ex); + this.logger.LogTrace("(-)[UNHANDLED_EXCEPTION]"); + throw; + } + }, + this.stakeCancellationTokenSource.Token, + repeatEvery: TimeSpan.FromMilliseconds(this.minerSleep), + startAfter: TimeSpans.Second); + + Interlocked.CompareExchange(ref this.currentState, (int)CurrentState.StakingInProgress, (int)CurrentState.StakingRequested); + } + + /// + public void StopStake() + { + if (Interlocked.CompareExchange(ref this.currentState, (int)CurrentState.StopStakingRequested, (int)CurrentState.StakingInProgress) != (int)CurrentState.StakingInProgress) + { + this.logger.LogTrace("(-)[STAKING_NOT_IN_PROGRESS]"); + return; + } + + this.stakeCancellationTokenSource?.Cancel(); + this.logger.LogDebug("Disposing staking loop."); + this.stakingLoop?.Dispose(); + this.stakingLoop = null; + this.stakeCancellationTokenSource?.Dispose(); + this.stakeCancellationTokenSource = null; + this.rpcGetStakingInfoModel.StopStaking(); + + Interlocked.CompareExchange(ref this.currentState, (int)CurrentState.Idle, (int)CurrentState.StopStakingRequested); + } + + /// + public async Task GenerateBlocksAsync(WalletSecret walletSecret, CancellationToken cancellationToken) + { + Guard.NotNull(walletSecret, nameof(walletSecret)); + + BlockTemplate blockTemplate = null; + + while (!cancellationToken.IsCancellationRequested) + { + // Prevent mining if the system time is not in sync with that of other members on the network. + if (this.timeSyncBehaviorState.IsSystemTimeOutOfSync) + { + this.logger.LogError("Staking cannot start, your system time does not match that of other nodes on the network." + Environment.NewLine + + "Please adjust your system time and restart the node."); + await Task.Delay(TimeSpan.FromMilliseconds(this.systemTimeOutOfSyncSleep), cancellationToken).ConfigureAwait(false); + continue; + } + + // Don't stake if the wallet is not up-to-date with the current chain. + if (this.consensusManager.Tip.HashBlock != this.walletManager.WalletTipHash) + { + this.logger.LogDebug("Waiting for wallet to catch up before mining can be started."); + + await Task.Delay(TimeSpan.FromMilliseconds(this.minerSleep), cancellationToken).ConfigureAwait(false); + continue; + } + + // Prevent staking if in initial block download. + if (this.initialBlockDownloadState.IsInitialBlockDownload()) + { + this.logger.LogDebug("Waiting for synchronization before mining can be started."); + + await Task.Delay(TimeSpan.FromMilliseconds(this.minerSleep), cancellationToken).ConfigureAwait(false); + continue; + } + + ChainedHeader chainTip = this.consensusManager.Tip; + + // Check if staking is allowed at this block height, if applicable + if (this.network.Consensus.Options is X1ConsensusOptions options) + { + var newBlockHeight = chainTip.Height + 1; + if (!options.IsAlgorithmAllowed(true, newBlockHeight)) + { + await Task.Delay(TimeSpan.FromMilliseconds(this.minerSleep), cancellationToken).ConfigureAwait(false); + continue; + } + } + + if (this.lastCoinStakeSearchPrevBlockHash != chainTip.HashBlock) + { + this.lastCoinStakeSearchPrevBlockHash = chainTip.HashBlock; + this.lastCoinStakeSearchTime = chainTip.Header.Time; + this.logger.LogDebug("New block '{0}' detected, setting last search time to its timestamp {1}.", chainTip, chainTip.Header.Time); + + // Reset the template as the chain advanced. + blockTemplate = null; + } + + uint coinstakeTimestamp = (uint)this.dateTimeProvider.GetAdjustedTimeAsUnixTimestamp() & ~this.network.Consensus.ProofOfStakeTimestampMask; + if (coinstakeTimestamp <= this.lastCoinStakeSearchTime) + { + this.logger.LogDebug("Current coinstake time {0} is not greater than last search timestamp {1}.", coinstakeTimestamp, this.lastCoinStakeSearchTime); + this.logger.LogTrace("(-)[NOTHING_TO_DO]"); + return; + } + + List utxoStakeDescriptions = this.GetUtxoStakeDescriptions(walletSecret, cancellationToken); + + blockTemplate = blockTemplate ?? this.blockProvider.BuildPosBlock(chainTip, new Script()); + var posBlock = (PosBlock)blockTemplate.Block; + + this.networkWeight = (long)this.GetNetworkWeight(); + this.rpcGetStakingInfoModel.CurrentBlockSize = posBlock.GetSerializedSize(); + this.rpcGetStakingInfoModel.CurrentBlockTx = posBlock.Transactions.Count(); + this.rpcGetStakingInfoModel.PooledTx = await this.mempoolLock.ReadAsync(() => this.mempool.MapTx.Count).ConfigureAwait(false); + this.rpcGetStakingInfoModel.Difficulty = this.GetDifficulty(chainTip); + this.rpcGetStakingInfoModel.NetStakeWeight = this.networkWeight; + + // Trying to create coinstake that satisfies the difficulty target, put it into a block and sign the block. + if (await this.StakeAndSignBlockAsync(utxoStakeDescriptions, blockTemplate, chainTip, blockTemplate.TotalFee, coinstakeTimestamp).ConfigureAwait(false)) + { + this.logger.LogDebug("New POS block created and signed successfully."); + await this.CheckStakeAsync(posBlock, chainTip).ConfigureAwait(false); + + blockTemplate = null; + } + else + { + this.logger.LogDebug("{0} failed to create POS block, waiting {1} ms for next round.", nameof(this.StakeAndSignBlockAsync), this.minerSleep); + await Task.Delay(TimeSpan.FromMilliseconds(this.minerSleep), cancellationToken).ConfigureAwait(false); + } + } + } + + internal List GetUtxoStakeDescriptions(WalletSecret walletSecret, CancellationToken cancellationToken) + { + var utxoStakeDescriptions = new List(); + + List stakableUtxos = this.walletManager + .GetSpendableTransactionsInWalletForStaking(walletSecret.WalletName, 1) + .Where(utxo => + { + if (this.minerSettings.EnforceStakingFlag) + { + if (utxo.Address.StakingExpiry == null) + return false; + + if (utxo.Address.StakingExpiry < this.dateTimeProvider.GetUtcNow()) + return false; + } + + return true; + }) + .Where(utxo => utxo.Transaction.Amount >= this.MinimumStakingCoinValue) // exclude dust from stake process + .ToList(); + + FetchCoinsResponse fetchedCoinSet = this.coinView.FetchCoins(stakableUtxos.Select(t => t.ToOutPoint()).Distinct().ToArray()); + Dictionary utxoByTransaction = fetchedCoinSet.UnspentOutputs.Where(utxo => utxo.Value.Coins != null).ToDictionary(utxo => utxo.Key, utxo => utxo.Value); + fetchedCoinSet = null; // allow GC to collect as soon as possible. + + for (int i = 0; i < stakableUtxos.Count; i++) + { + UnspentOutputReference stakableUtxo = stakableUtxos[i]; + + if (cancellationToken.IsCancellationRequested) + { + this.logger.LogTrace("(-)[CANCELLATION]"); + throw new OperationCanceledException(cancellationToken); + } + + UnspentOutput coinSet = utxoByTransaction.TryGet(stakableUtxo.ToOutPoint()); + if (coinSet?.Coins == null) + continue; + + TxOut utxo = coinSet.Coins.TxOut; + if ((utxo == null) || (utxo.Value < this.MinimumStakingCoinValue)) + continue; + + uint256 hashBlock = this.chainIndexer.GetHeader((int)coinSet.Coins.Height)?.HashBlock; + if (hashBlock == null) + continue; + + var utxoStakeDescription = new UtxoStakeDescription + { + TxOut = utxo, + OutPoint = coinSet.OutPoint, + Address = stakableUtxo.Address, + HashBlock = hashBlock, + UtxoSet = coinSet, + Secret = walletSecret // Temporary. + }; + utxoStakeDescriptions.Add(utxoStakeDescription); + + this.logger.LogDebug("UTXO '{0}' with value {1} might be available for staking.", utxoStakeDescription.OutPoint, utxo.Value); + } + + this.logger.LogDebug("Wallet total staking balance is {0}.", new Money(utxoStakeDescriptions.Sum(d => d.TxOut.Value))); + return utxoStakeDescriptions; + } + + /// + /// Once a new block is staked, this method is used to verify that it + /// is a valid block and if so, it will add it to the chain. + /// + /// The new block. + /// Block that was considered as a chain tip when the block staking started. + private async Task CheckStakeAsync(Block block, ChainedHeader chainTip) + { + if (!BlockStake.IsProofOfStake(block)) + { + this.logger.LogTrace("(-)[NOT_POS]"); + return; + } + + // Verify hash target and signature of coinstake tx. + BlockStake prevBlockStake = this.stakeChain.Get(chainTip.HashBlock); + if (prevBlockStake == null) + { + this.logger.LogTrace("(-)[NO_PREV_STAKE]"); + ConsensusErrors.PrevStakeNull.Throw(); + } + + // Validate the block. + ChainedHeader chainedHeader = await this.consensusManager.BlockMinedAsync(block).ConfigureAwait(false); + + if (chainedHeader == null) + { + this.logger.LogTrace("(-)[REORG-2]"); + return; + } + + this.logger.LogInformation("=================================================================="); + this.logger.LogInformation("Found new POS block hash '{0}' at height {1}.", chainedHeader.HashBlock, chainedHeader.Height); + this.logger.LogInformation("=================================================================="); + } + + /// + /// Attempts to find a POS staking solution and if it succeeds, then it completes a block + /// to be mined and signes it. + /// + /// List of UTXOs that are available in the wallet for staking. + /// Template of the block that we are trying to mine. + /// Tip of the best chain. + /// Transaction fees from the transactions included in the block if we mine it. + /// Maximal timestamp of the coinstake transaction. The actual timestamp can be lower, but not higher. + /// true if the function succeeds, false otherwise. + private async Task StakeAndSignBlockAsync(List utxoStakeDescriptions, BlockTemplate blockTemplate, ChainedHeader chainTip, long fees, uint coinstakeTimestamp) + { + var block = blockTemplate.Block as PosBlock; + + // If we are trying to sign something except proof-of-stake block template. + if (!block.Transactions[0].Outputs[0].IsEmpty) + { + this.logger.LogTrace("(-)[NO_POS_BLOCK]:false"); + return false; + } + + // If we are trying to sign a complete proof-of-stake block. + if (BlockStake.IsProofOfStake(block)) + { + this.logger.LogTrace("(-)[ALREADY_DONE]:true"); + return true; + } + + var coinstakeContext = new CoinstakeContext { CoinstakeTx = this.network.CreateTransaction() }; + coinstakeContext.StakeTimeSlot = coinstakeTimestamp; + + // Search to current coinstake time. + long searchTime = coinstakeContext.StakeTimeSlot; + + long searchInterval = searchTime - this.lastCoinStakeSearchTime; + this.rpcGetStakingInfoModel.SearchInterval = (int)searchInterval; + + this.lastCoinStakeSearchTime = searchTime; + this.logger.LogDebug("Search interval set to {0}, last coinstake search timestamp set to {1}.", searchInterval, this.lastCoinStakeSearchTime); + + if (await this.CreateCoinstakeAsync(utxoStakeDescriptions, blockTemplate, chainTip, searchInterval, fees, coinstakeContext).ConfigureAwait(false)) + { + uint minTimestamp = chainTip.Header.Time + 1; + if (coinstakeContext.StakeTimeSlot >= minTimestamp) + { + // Make sure coinstake would meet timestamp protocol as it would be the same as the block timestamp. + block.Header.Time = coinstakeContext.StakeTimeSlot; + if (block.Transactions[0] is IPosTransactionWithTime posTrx) + { + posTrx.Time = coinstakeContext.StakeTimeSlot; + } + + block.Transactions.Insert(1, coinstakeContext.CoinstakeTx); + + // The coinstake was added to the block so we need to regenerate the witness commitment. + this.blockProvider.BlockModified(chainTip, block); + + // Append a signature to our block. + ECDSASignature signature = coinstakeContext.Key.Sign(block.GetHash()); + + block.BlockSignature = new BlockSignature { Signature = signature.ToDER() }; + return true; + } + else this.logger.LogDebug("Coinstake transaction created with too early timestamp {0}, minimal timestamp is {1}.", coinstakeContext.StakeTimeSlot, minTimestamp); + } + else this.logger.LogDebug("Unable to create coinstake transaction."); + + return false; + } + + /// + public async Task CreateCoinstakeAsync(List utxoStakeDescriptions, BlockTemplate blockTemplate, ChainedHeader chainTip, long searchInterval, long fees, CoinstakeContext coinstakeContext) + { + coinstakeContext.CoinstakeTx.Inputs.Clear(); + coinstakeContext.CoinstakeTx.Outputs.Clear(); + + // Mark coinstake transaction. + coinstakeContext.CoinstakeTx.Outputs.Add(new TxOut(Money.Zero, new Script())); + + (Money balance, Money immature) = await this.GetMatureBalanceAsync(utxoStakeDescriptions).ConfigureAwait(false); + this.rpcGetStakingInfoModel.Immature = immature.Satoshi; + + if (balance <= this.targetReserveBalance) + { + this.rpcGetStakingInfoModel.PauseStaking(); + + this.logger.LogDebug("Total balance of available UTXOs is {0}, which is less than or equal to reserve balance {1}.", balance, this.targetReserveBalance); + this.logger.LogTrace("(-)[BELOW_RESERVE]:false"); + return false; + } + + // Select UTXOs with suitable depth. + List stakingUtxoDescriptions = await this.GetUtxoStakeDescriptionsSuitableForStakingAsync(utxoStakeDescriptions, chainTip, coinstakeContext.StakeTimeSlot, balance - this.targetReserveBalance).ConfigureAwait(false); + if (!stakingUtxoDescriptions.Any()) + { + this.rpcGetStakingInfoModel.PauseStaking(); + + this.logger.LogTrace("(-)[NO_SELECTION]:false"); + return false; + } + + long ourWeight = stakingUtxoDescriptions.Sum(s => s.TxOut.Value); + long expectedTime = ((uint)this.network.Consensus.TargetSpacing.TotalSeconds) * this.networkWeight / ourWeight; + decimal ourPercent = this.networkWeight != 0 ? 100.0m * (decimal)ourWeight / (decimal)this.networkWeight : 0; + + this.logger.LogInformation("Node staking with {0} ({1:0.00} % of the network weight {2}), est. time to find new block is {3}.", new Money(ourWeight), ourPercent, new Money(this.networkWeight), TimeSpan.FromSeconds(expectedTime)); + + this.rpcGetStakingInfoModel.ResumeStaking(ourWeight, expectedTime); + + long minimalAllowedTime = chainTip.Header.Time + 1; + this.logger.LogDebug("Trying to find staking solution among {0} transactions, minimal allowed time is {1}, coinstake time is {2}.", stakingUtxoDescriptions.Count, minimalAllowedTime, coinstakeContext.StakeTimeSlot); + + // If the time after applying the mask is lower than minimal allowed time, + // it is simply too early for us to mine, there can't be any valid solution. + if ((coinstakeContext.StakeTimeSlot & ~this.network.Consensus.ProofOfStakeTimestampMask) < minimalAllowedTime) + { + this.logger.LogTrace("(-)[TOO_EARLY_TIME_AFTER_LAST_BLOCK]:false"); + return false; + } + + // Create worker tasks that will look for kernel. + // Run task in parallel using the default task scheduler. + int coinIndex = 0; + int workerCount = (stakingUtxoDescriptions.Count + UtxoStakeDescriptionsPerCoinstakeWorker - 1) / UtxoStakeDescriptionsPerCoinstakeWorker; + var workers = new Task[workerCount]; + var workerContexts = new CoinstakeWorkerContext[workerCount]; + + var workersResult = new CoinstakeWorkerResult(); + for (int workerIndex = 0; workerIndex < workerCount; workerIndex++) + { + var cwc = new CoinstakeWorkerContext + { + Index = workerIndex, + Logger = this.loggerFactory.CreateLogger(this.GetType().FullName), + utxoStakeDescriptions = new List(), + CoinstakeContext = coinstakeContext, + Result = workersResult + }; + + int stakingUtxoCount = Math.Min(stakingUtxoDescriptions.Count - coinIndex, UtxoStakeDescriptionsPerCoinstakeWorker); + cwc.utxoStakeDescriptions.AddRange(stakingUtxoDescriptions.GetRange(coinIndex, stakingUtxoCount)); + coinIndex += stakingUtxoCount; + workerContexts[workerIndex] = cwc; + } + + await Task.Run(() => Parallel.ForEach(workerContexts, cwc => + { + this.CoinstakeWorker(cwc, chainTip, blockTemplate.Block, minimalAllowedTime, searchInterval); + })); + + if (workersResult.KernelFoundIndex == CoinstakeWorkerResult.KernelNotFound) + { + this.logger.LogTrace("(-)[KERNEL_NOT_FOUND]:false"); + return false; + } + + this.logger.LogDebug("Worker #{0} found the kernel.", workersResult.KernelFoundIndex); + + // We have to make sure that we have no future timestamps in our transactions set. + // We ignore the coinbase (it gets its timestamp reset after the coinstake is created). + for (int i = blockTemplate.Block.Transactions.Count - 1; i >= 1; i--) + { + // We have not yet updated the header timestamp, so we use the coinstake timestamp directly here. + if (blockTemplate.Block.Transactions[i] is IPosTransactionWithTime posTrx) + { + if (posTrx.Time <= coinstakeContext.StakeTimeSlot) + continue; + + // Update the total fees, with the to-be-removed transaction taken into account. + fees -= blockTemplate.FeeDetails[blockTemplate.Block.Transactions[i].GetHash()].Satoshi; + + this.logger.LogDebug("Removing transaction with timestamp {0} as it is greater than coinstake transaction timestamp {1}. New fee amount {2}.", posTrx.Time, coinstakeContext.StakeTimeSlot, fees); + blockTemplate.Block.Transactions.Remove(blockTemplate.Block.Transactions[i]); + } + } + + // Get reward for newly created block. + long reward = fees + this.consensusManager.ConsensusRules.GetRule().GetProofOfStakeReward(chainTip.Height + 1); + if (reward < 0) + { + // TODO: This can't happen unless we remove reward for mined block. + // If this can happen over time then this check could be done much sooner + // to avoid a lot of computation. + this.logger.LogTrace("(-)[NO_REWARD]:false"); + return false; + } + + // Input to coinstake transaction. + UtxoStakeDescription coinstakeInput = workersResult.KernelCoin; + + // Total amount of input values in coinstake transaction. + long coinstakeOutputValue = coinstakeInput.TxOut.Value + reward; + + int eventuallyStakableUtxosCount = utxoStakeDescriptions.Count; + Transaction coinstakeTx = this.PrepareCoinStakeTransactions(chainTip.Height, coinstakeContext, coinstakeOutputValue, eventuallyStakableUtxosCount, ourWeight); + + if (coinstakeTx is IPosTransactionWithTime posTrxn) + { + posTrxn.Time = coinstakeContext.StakeTimeSlot; + } + + // Sign. + if (!this.SignTransactionInput(coinstakeInput, coinstakeTx)) + { + this.logger.LogTrace("(-)[SIGN_FAILED]:false"); + return false; + } + + // Limit size. + int serializedSize = coinstakeContext.CoinstakeTx.GetSerializedSize(this.network.Consensus.ConsensusFactory, SerializationType.Network); + if (serializedSize >= (MaxBlockSizeGen / 5)) + { + this.logger.LogDebug("Coinstake size {0} bytes exceeded limit {1} bytes.", serializedSize, MaxBlockSizeGen / 5); + this.logger.LogTrace("(-)[SIZE_EXCEEDED]:false"); + return false; + } + + // Successfully generated coinstake. + return true; + } + + internal Transaction PrepareCoinStakeTransactions(int currentChainHeight, CoinstakeContext coinstakeContext, long coinstakeOutputValue, int utxosCount, long amountStaked) + { + // Split stake into SplitFactor utxos if above threshold. + bool shouldSplitStake = this.ShouldSplitStake(utxosCount, amountStaked, coinstakeOutputValue, currentChainHeight); + + int lastOutputIndex = coinstakeContext.CoinstakeTx.Outputs.Count - 1; + + if (!shouldSplitStake) + { + coinstakeContext.CoinstakeTx.Outputs[lastOutputIndex].Value = coinstakeOutputValue; + this.logger.LogDebug("Coinstake output value is {0}.", coinstakeContext.CoinstakeTx.Outputs[lastOutputIndex].Value); + this.logger.LogTrace("(-)[NO_SPLIT]:{0}", coinstakeContext.CoinstakeTx); + return coinstakeContext.CoinstakeTx; + } + + long splitValue = coinstakeOutputValue / SplitFactor; + long remainder = coinstakeOutputValue - (SplitFactor - 1) * splitValue; + coinstakeContext.CoinstakeTx.Outputs[lastOutputIndex].Value = remainder; + + for (int i = 0; i < SplitFactor - 1; i++) + { + var split = new TxOut(splitValue, coinstakeContext.CoinstakeTx.Outputs[lastOutputIndex].ScriptPubKey); + coinstakeContext.CoinstakeTx.Outputs.Add(split); + } + + this.logger.LogTrace("Coinstake output value has been split into {0} outputs of {1} and a remainder of {2}.", SplitFactor - 1, splitValue, remainder); + + return coinstakeContext.CoinstakeTx; + } + + /// + /// Worker method that tries to find coinstake kernel within a small list of UTXOs. + /// + /// There are multiple worker tasks created, each checking subset of all available UTXOs. + /// This allows the kernel finding task to be processed on multiple processors in parallel. + /// + /// + /// Context information with worker task description. Results of the worker's attempt are also stored in this context. + /// Tip of the best chain. Used only to stop working as soon as the chain advances. + /// Template of the block that we are trying to mine. + /// Minimal valid timestamp for new coinstake transaction. + /// Length of an unexplored block time space in seconds. It only makes sense to look for a solution within this interval. + private void CoinstakeWorker(CoinstakeWorkerContext context, ChainedHeader chainTip, Block block, long minimalAllowedTime, long searchInterval) + { + context.Logger.LogDebug("Going to process {0} UTXOs.", context.utxoStakeDescriptions.Count); + + // Sort staking UTXOs by amount, so that highest amounts are tried first + // because they have greater chance to succeed and thus saving some work. + List orderedUtxoStakeDescriptions = context.utxoStakeDescriptions.OrderByDescending(o => o.TxOut.Value).ToList(); + + bool stopWork = false; + foreach (UtxoStakeDescription utxoStakeInfo in orderedUtxoStakeDescriptions) + { + context.Logger.LogDebug("Trying UTXO from address '{0}', output amount {1}.", utxoStakeInfo.Address.Address, utxoStakeInfo.TxOut.Value); + + // Script of the first coinstake input. + Script scriptPubKeyKernel = utxoStakeInfo.TxOut.ScriptPubKey; + if (!this.ValidStakingTemplates.Any(a => a.Value.CheckScriptPubKey(scriptPubKeyKernel))) + { + context.Logger.LogDebug("Kernel type must be {0}, kernel rejected.", string.Join(" or ", this.ValidStakingTemplates.Keys)); + continue; + } + + for (uint n = 0; n < searchInterval; n++) + { + if (context.Result.KernelFoundIndex != CoinstakeWorkerResult.KernelNotFound) + { + context.Logger.LogDebug("Different worker #{0} already found kernel, stopping work.", context.Result.KernelFoundIndex); + stopWork = true; + break; + } + + if (this.stakeCancellationTokenSource.Token.IsCancellationRequested) + { + context.Logger.LogDebug("Application shutdown detected, stopping work."); + stopWork = true; + break; + } + + if (chainTip != this.chainIndexer.Tip) + { + context.Logger.LogDebug("Chain advanced, stopping work."); + stopWork = true; + break; + } + + uint txTime = context.CoinstakeContext.StakeTimeSlot - n; + + // Once we reach previous block time + 1, we can't go any lower + // because it is required that the block time is greater than the previous block time. + if (txTime < minimalAllowedTime) + break; + + if ((txTime & this.network.Consensus.ProofOfStakeTimestampMask) != 0) + continue; + + context.Logger.LogDebug("Trying with transaction time {0}.", txTime); + + try + { + OutPoint prevoutStake = utxoStakeInfo.OutPoint;// new OutPoint(utxoStakeInfo.UtxoSet.TransactionId, utxoStakeInfo.OutPoint.N); + + var contextInformation = new PosRuleContext(BlockStake.Load(block)); + + var validKernel = this.stakeValidator.CheckKernel(contextInformation, chainTip, block.Header.Bits, txTime, prevoutStake); + + if (!validKernel) + { + continue; + } + + if (context.Result.SetKernelFoundIndex(context.Index)) + { + context.Logger.LogDebug("Kernel found with solution hash '{0}'.", contextInformation.HashProofOfStake); + + Wallet wallet = this.walletManager.GetWalletByName(utxoStakeInfo.Secret.WalletName); + context.CoinstakeContext.Key = wallet.GetExtendedPrivateKeyForAddress(utxoStakeInfo.Secret.WalletPassword, utxoStakeInfo.Address).PrivateKey; + utxoStakeInfo.Key = context.CoinstakeContext.Key; + + context.CoinstakeContext.StakeTimeSlot = txTime; + context.CoinstakeContext.CoinstakeTx.AddInput(new TxIn(prevoutStake)); + Script scriptPubKeyOut; + + // Create a pubkey script form the current script. + string scriptType = this.ValidStakingTemplates.Single(t => t.Value.CheckScriptPubKey(utxoStakeInfo.TxOut.ScriptPubKey)).Key; + + // Default behavior. + if ((scriptType == "P2PK") || (scriptType == "P2PKH")) + { + scriptPubKeyOut = PayToPubkeyTemplate.Instance.GenerateScriptPubKey(context.CoinstakeContext.Key.PubKey); + } + else + // Support for otherwise unsupported script types. + { + context.CoinstakeContext.CoinstakeTx.Outputs.Add(new TxOut(Money.Zero, + new Script(OpcodeType.OP_RETURN, Op.GetPushOp(utxoStakeInfo.Key.PubKey.Compress().ToBytes())))); + + scriptPubKeyOut = utxoStakeInfo.TxOut.ScriptPubKey; + } + + context.CoinstakeContext.CoinstakeTx.Outputs.Add(new TxOut(0, scriptPubKeyOut)); + context.Result.KernelCoin = utxoStakeInfo; + + context.Logger.LogDebug("Kernel accepted, coinstake input is '{0}', stopping work.", prevoutStake); + } + else context.Logger.LogDebug("Kernel found, but worker #{0} announced its kernel earlier, stopping work.", context.Result.KernelFoundIndex); + + stopWork = true; + } + catch (ConsensusErrorException cex) + { + context.Logger.LogDebug("Checking kernel failed with exception: {0}.", cex.Message); + stopWork = true; + } + + if (stopWork) break; + } + + // If kernel is found or error occurred, stop searching. + if (stopWork) break; + } + } + + /// + /// Signs input of a transaction. + /// + /// Transaction input. + /// Transaction being built. + /// true if the function succeeds, false otherwise. + private bool SignTransactionInput(UtxoStakeDescription input, Transaction transaction) + { + bool res = false; + try + { + TransactionBuilder transactionBuilder = new TransactionBuilder(this.network) + .AddKeys(input.Key); + + if (PayToScriptHashTemplate.Instance.CheckScriptPubKey(input.TxOut.ScriptPubKey) || + PayToWitScriptHashTemplate.Instance.CheckScriptPubKey(input.TxOut.ScriptPubKey)) + { + if (input.Address.RedeemScript == null) + throw new MinerException("Redeem script does not match output"); + + var scriptCoin = ScriptCoin.Create(this.network, input.OutPoint, input.TxOut, input.Address.RedeemScript); + + transactionBuilder.AddCoins(scriptCoin); + } + else + { + transactionBuilder.AddCoins(new Coin(input.OutPoint, input.TxOut)); + } + + foreach (BuilderExtension extension in this.walletManager.GetTransactionBuilderExtensionsForStaking()) + transactionBuilder.Extensions.Add(extension); + + transactionBuilder.SignTransactionInPlace(transaction); + + res = true; + } + catch (Exception e) + { + this.logger.LogDebug("Exception occurred: {0}", e.ToString()); + } + + return res; + } + + /// + public async Task<(Money balance, Money immature)> GetMatureBalanceAsync(List utxoStakeDescriptions) + { + var money = new Money(0); + var immature = new Money(0); + + foreach (UtxoStakeDescription utxoStakeDescription in utxoStakeDescriptions) + { + // Must wait until coinbase is safely deep enough in the chain before valuing it. + if ((utxoStakeDescription.UtxoSet.Coins.IsCoinbase || utxoStakeDescription.UtxoSet.Coins.IsCoinstake) && (await this.GetBlocksCountToMaturityAsync(utxoStakeDescription).ConfigureAwait(false) > 0)) + { + immature += utxoStakeDescription.TxOut.Value; + continue; + } + + money += utxoStakeDescription.TxOut.Value; + } + + return (money, immature); + } + + /// + /// Selects UTXOs that are suitable for staking. + /// + /// Such a UTXO has to be confirmed with enough confirmations - i.e. has suitable depth, + /// and it also has to be mature and meet requirement for minimal value. + /// + /// + /// List of UTXO descriptions that are candidates for being used for staking. + /// Tip of the best chain. + /// Timestamp of the coinstake transaction. + /// Target money amount of UTXOs that can be used for staking. + /// List of UTXO descriptions that meet the requirements for staking. + internal async Task> GetUtxoStakeDescriptionsSuitableForStakingAsync(List utxoStakeDescriptions, ChainedHeader chainTip, uint spendTime, long targetValue) + { + var res = new List(); + + long currentValue = 0; + // Add 1 to chainTip because this is being called in the context of trying to create a new block, which will have height (chainTip + 1). + // Subtract 1 from the required depth, because if a UTXO is in a block with height == requiredDepth it already has 1 confirmation. + // e.g. consider a hypothetical chain with the coinstake age requirement = 5, a prospective UTXO at height 3 and a chainTip of 10. + // Taking (chainTip + 1) into account, the UTXO has ((10 + 1) - 3 + 1) = 9 confirmations, meaning it easily has sufficient depth. + // Now consider a different UTXO at height 7. This has (10 + 1) - 7 + 1) = 5 confirmations, meaning it just barely qualifies. + long requiredDepth = ((PosConsensusOptions)this.network.Consensus.Options).GetStakeMinConfirmations(chainTip.Height + 1, this.network) - 1; + foreach (UtxoStakeDescription utxoStakeDescription in utxoStakeDescriptions.OrderByDescending(x => x.TxOut.Value)) + { + // Internally GetDepthInMainChainAsync uses chainTip instead of (chainTip + 1). That is why there is a later comparison to + // (depth < requiredDepth) instead of (depth <= requiredDepth). + int depth = await this.GetDepthInMainChainAsync(utxoStakeDescription).ConfigureAwait(false); + this.logger.LogDebug("Checking if UTXO '{0}' value {1} can be added, its depth is {2}.", utxoStakeDescription.OutPoint, utxoStakeDescription.TxOut.Value, depth); + + if (depth < 1) + { + this.logger.LogDebug("UTXO '{0}' is new or reorg happened.", utxoStakeDescription.OutPoint); + continue; + } + + if (depth < requiredDepth) + { + this.logger.LogDebug("UTXO '{0}' depth {1} is lower than required minimum depth {2}.", utxoStakeDescription.OutPoint, depth, requiredDepth); + continue; + } + + if (utxoStakeDescription.UtxoSet.Coins.Time > spendTime) + { + this.logger.LogDebug("UTXO '{0}' can't be added because its time {1} is greater than coinstake time {2}.", utxoStakeDescription.OutPoint, utxoStakeDescription.UtxoSet.Coins.Time, spendTime); + continue; + } + + // Under normal circumstances this maturity check will not trigger, as the requiredDepth calculation will have already filtered out + // a coinbase/coinstake with insufficient confirmations. However, see the comments within the GetBlocksCountToMaturityAsync method + // for the rationale of why we perform this check anyway. + int toMaturity = await this.GetBlocksCountToMaturityAsync(utxoStakeDescription).ConfigureAwait(false); + if (toMaturity > 0) + { + this.logger.LogDebug("UTXO '{0}' can't be added because it is not mature, {1} blocks to maturity left.", utxoStakeDescription.OutPoint, toMaturity); + continue; + } + + currentValue += utxoStakeDescription.TxOut.Value; + + this.logger.LogDebug("UTXO '{0}' accepted.", utxoStakeDescription.OutPoint); + res.Add(utxoStakeDescription); + + if (currentValue >= targetValue) + break; + } + + return res; + } + + /// + /// Calculates the number of blocks until a coinbase or coinstake UTXO is considered mature for staking. + /// + /// The UTXO stake description. + /// How many blocks are left till UTXO is considered mature for staking. + /// Do NOT use this for general-purpose maturity calculations outside of as it will give off-by-one errors. + /// This method is making the assumption that we are adding a new block to the chain, and thus reduces the maturity threshold by 1. + private async Task GetBlocksCountToMaturityAsync(UtxoStakeDescription utxoStakeDescription) + { + // The concept of maturity only applies to coinbase and coinstake outputs, so normal outputs do not have this restriction. + if (!(utxoStakeDescription.UtxoSet.Coins.IsCoinbase || utxoStakeDescription.UtxoSet.Coins.IsCoinstake)) + return 0; + + // Using CoinbaseMaturity here is not strictly correct. Due to the ProvenHeaderCoinstakeRule enforcing a unilateral prevOut depth equivalent + // to maxReorg, a mature UTXO is not necessarily old enough for staking. However, the minter also separately filters out UTXOs younger than + // the minimum required depth in the GetUtxoStakeDescriptionsSuitableForStakingAsync method. So this method retains the CoinbaseMaturity + // constant so that it returns an intuitive result, in case there are alternate network definitions where the coinstake age requirement is + // less than the maturity. + int minConf = (int)this.network.Consensus.CoinbaseMaturity; + + // The reason why we subtract 1 here is because any newly staked block will be at (chainTip + 1), effectively giving an extra confirmation over and above the depth calculation. + return Math.Max(0, minConf - 1 - await this.GetDepthInMainChainAsync(utxoStakeDescription).ConfigureAwait(false)); + } + + /// + /// Gets depth of transaction in blockchain. + /// + /// The UTXO stake description. + /// + /// -1 if not in blockchain, and not in memory pool (conflicted transaction). + /// 0 if in memory pool, waiting to be included in a block. + /// Value greater than 1 if included in a block. Shows how many blocks deep in the main chain. + /// + private async Task GetDepthInMainChainAsync(UtxoStakeDescription utxoStakeDescription) + { + ChainedHeader chainedBlock = this.chainIndexer.GetHeader(utxoStakeDescription.HashBlock); + + if (chainedBlock == null) + return await this.mempoolLock.ReadAsync(() => this.mempool.Exists(utxoStakeDescription.UtxoSet.OutPoint.Hash) ? 0 : -1).ConfigureAwait(false); + + // Add 1 because a transaction is considered to have 1 confirmation when it is in a block. + return this.chainIndexer.Tip.Height - chainedBlock.Height + 1; + } + + /// + public double GetDifficulty(ChainedHeader block) + { + double res = 1.0; + + if (block == null) + { + // Use consensus loop's tip rather than concurrent chain's tip + // because consensus loop's tip is guaranteed to have block stake in the database. + ChainedHeader tip = this.consensusManager.Tip; + if (tip == null) + { + this.logger.LogTrace("(-)[DEFAULT]:{0}", res); + return res; + } + + block = this.stakeValidator.GetLastPowPosChainedBlock(this.stakeChain, tip, false); + } + + // calculate the current shift value. + uint shift = (block.Header.Bits >> 24) & 0xFF; + double diff = (double)0x0000FFFF / (double)(block.Header.Bits & 0x00FFFFFF); + + // shift the difficulty up. + while (shift < 29) + { + diff *= 256.0; + shift++; + } + + // shift the difficulty down. + while (shift > 29) + { + diff /= 256.0; + shift--; + } + + res = diff; + return res; + } + + /// + public double GetNetworkWeight() + { + int interval = 72; + double stakeKernelsAvg = 0.0; + int stakesHandled = 0; + long stakesTime = 0; + + // Use consensus loop's tip rather than concurrent chain's tip + // because consensus loop's tip is guaranteed to have block stake in the database. + ChainedHeader block = this.consensusManager.Tip; + ChainedHeader prevStakeBlock = null; + + double res = 0.0; + while ((block != null) && (stakesHandled < interval)) + { + BlockStake blockStake = this.stakeChain.Get(block.HashBlock); + if (blockStake != null && blockStake.IsProofOfStake()) + { + if (prevStakeBlock != null) + { + stakeKernelsAvg += this.GetDifficulty(prevStakeBlock) * (double)0x100000000; + stakesTime += (long)prevStakeBlock.Header.Time - (long)block.Header.Time; + stakesHandled++; + } + + prevStakeBlock = block; + } + + block = block.Previous; + } + + if (stakesTime != 0) res = stakeKernelsAvg / stakesTime; + + res *= this.network.Consensus.ProofOfStakeTimestampMask + 1; + + return res; + } + + /// + public GetStakingInfoModel GetGetStakingInfoModel() + { + return (GetStakingInfoModel)this.rpcGetStakingInfoModel.Clone(); + } + + /// + /// Checks whether the coinstake should be split or not. + /// + /// Number of UTXOs that the wallet could stake, if coin base maturity and stake minimum confirmations were not taken into account. + /// Total amount currently at stake. + /// Value of the coin we are considering to split. + /// Current height of the chain. + /// true if the coinstake should be split, false otherwise. + /// + /// We do not split a coin if the value of new coins after the split would be less than . Because we split the coin to multiple outputs defined by split factor, we only consider coins with value at least * . + /// + /// If the above-mentioned criteria is satisfied, then we split the coin if its value is greater than an expected average value of coins that we would have if we have perfect distribution of the value among all our coins while having a specific number of coins that we aim for. The optimal number of coins we are looking for is calculated based on consensus settings of coin maturity and minimum required coin age for staking. + /// + /// + /// + /// + internal bool ShouldSplitStake(int stakedUtxosCount, long amountStaked, long coinValue, int chainHeight) + { + if (!this.CoinstakeSplitEnabled) + { + this.logger.LogTrace("(-)[SPLITTING_DISABLED]:{0}", false); + return false; + } + + long coinAgeLimit = ((PosConsensusOptions)this.network.Consensus.Options).GetStakeMinConfirmations(chainHeight + 1, this.network); + long coinMaturityLimit = this.network.Consensus.CoinbaseMaturity; + long requiredCoinAgeForStaking = Math.Max(coinMaturityLimit, coinAgeLimit); + this.logger.LogDebug("Required coin age for staking is {0}.", requiredCoinAgeForStaking); + + long targetCoinDistributionSize = (requiredCoinAgeForStaking + 1) * CoinstakeSplitLimitMultiplier; + + bool coinAboveMinValue = coinValue > SplitFactor * (long)this.MinimumSplitCoinValue; + bool coinAboveTargetAverage = coinValue > (amountStaked / targetCoinDistributionSize) + Money.COIN; + + bool shouldSplitCoin = coinAboveMinValue && coinAboveTargetAverage; + + return shouldSplitCoin; + } + } +} diff --git a/src/Networks/Blockcore.Networks.X1/Components/X1PowMining.cs b/src/Networks/Blockcore.Networks.X1/Components/X1PowMining.cs new file mode 100644 index 000000000..4d3a91123 --- /dev/null +++ b/src/Networks/Blockcore.Networks.X1/Components/X1PowMining.cs @@ -0,0 +1,777 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Blockcore.AsyncWork; +using Blockcore.Consensus; +using Blockcore.Consensus.BlockInfo; +using Blockcore.Consensus.Chain; +using Blockcore.Consensus.ScriptInfo; +using Blockcore.Features.MemoryPool; +using Blockcore.Features.MemoryPool.Interfaces; +using Blockcore.Features.Miner; +using Blockcore.Features.Miner.Interfaces; +using Blockcore.Interfaces; +using Blockcore.Mining; +using Blockcore.Networks; +using Blockcore.Networks.X1.Consensus; +using Blockcore.Utilities; +using Microsoft.Extensions.Logging; +using NBitcoin; +using NBitcoin.BouncyCastle.Math; +using NBitcoin.Crypto; + +namespace Blockcore.Networks.X1.Components +{ + public class X1PowMining : IPowMining + { + /// Factory for creating background async loop tasks. + private readonly IAsyncProvider asyncProvider; + + /// Builder that creates a proof-of-work block template. + private readonly IBlockProvider blockProvider; + + /// Thread safe chain of block headers from genesis. + private readonly ChainIndexer chainIndexer; + + /// Manager of the longest fully validated chain of blocks. + private readonly IConsensusManager consensusManager; + + /// Provider of time functions. + private readonly IDateTimeProvider dateTimeProvider; + + private uint256 hashPrevBlock; + + private const int InnerLoopCount = 0x10000; + + /// Instance logger. + private readonly ILogger logger; + + /// Factory for creating loggers. + private readonly ILoggerFactory loggerFactory; + + private readonly IInitialBlockDownloadState initialBlockDownloadState; + + /// Transaction memory pool for managing transactions in the memory pool. + private readonly ITxMempool mempool; + + /// A lock for managing asynchronous access to memory pool. + private readonly MempoolSchedulerLock mempoolLock; + + /// The async loop we need to wait upon before we can shut down this feature. + private IAsyncLoop miningLoop; + + /// Specification of the network the node runs on - regtest/testnet/mainnet. + private readonly Network network; + + /// Global application life cycle control - triggers when application shuts down. + private readonly INodeLifetime nodeLifetime; + + /// SpartaCrypt OpenCL Miner. + private readonly OpenCLMiner openCLMiner; + + /// SpartaCrypt OpenCL Miner. + private readonly X1MinerSettings minerSettings; + + /// Constant for hash rate calculation. + readonly BigInteger pow256 = BigInteger.ValueOf(2).Pow(256); + + /// Stopwatch for hash rate calculation. + readonly Stopwatch stopwatch = new Stopwatch(); + + /// + /// A cancellation token source that can cancel the mining processes and is linked to the . + /// + private CancellationTokenSource miningCancellationTokenSource; + + public X1PowMining( + IAsyncProvider asyncProvider, + IBlockProvider blockProvider, + IConsensusManager consensusManager, + ChainIndexer chainIndexer, + IDateTimeProvider dateTimeProvider, + ITxMempool mempool, + MempoolSchedulerLock mempoolLock, + Network network, + INodeLifetime nodeLifetime, + ILoggerFactory loggerFactory, + IInitialBlockDownloadState initialBlockDownloadState, + MinerSettings minerSettings) + { + this.asyncProvider = asyncProvider; + this.blockProvider = blockProvider; + this.chainIndexer = chainIndexer; + this.consensusManager = consensusManager; + this.dateTimeProvider = dateTimeProvider; + this.loggerFactory = loggerFactory; + this.initialBlockDownloadState = initialBlockDownloadState; + this.logger = loggerFactory.CreateLogger(this.GetType().FullName); + this.mempool = mempool; + this.mempoolLock = mempoolLock; + this.network = network; + this.nodeLifetime = nodeLifetime; + this.miningCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(new[] { this.nodeLifetime.ApplicationStopping }); + this.minerSettings = (X1MinerSettings)minerSettings; + if (this.minerSettings.UseOpenCL) + { + this.openCLMiner = new OpenCLMiner(this.minerSettings, loggerFactory); + } + } + + /// + public void Mine(Script reserveScript) + { + if (this.miningLoop != null) + return; + + this.miningCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(new[] { this.nodeLifetime.ApplicationStopping }); + + this.miningLoop = this.asyncProvider.CreateAndRunAsyncLoop("PowMining.Mine", token => + { + try + { + this.GenerateBlocks(new ReserveScript { ReserveFullNodeScript = reserveScript }, int.MaxValue, int.MaxValue); + } + catch (OperationCanceledException) + { + // Application stopping, nothing to do as the loop will be stopped. + } + catch (MinerException me) + { + this.logger.LogWarning($"{nameof(MinerException)} in mining loop: {me}"); + } + catch (ConsensusErrorException cee) + { + this.logger.LogWarning($"{nameof(ConsensusErrorException)} in mining loop: {cee}"); + } + catch (ConsensusException ce) + { + this.logger.LogWarning($"{nameof(ConsensusException)} in mining loop: {ce}"); + } + catch (Exception e) + { + this.logger.LogError($"{e.GetType()} in mining loop, exiting mining loop: {e}"); + throw; + } + + return Task.CompletedTask; + }, + this.miningCancellationTokenSource.Token, + repeatEvery: TimeSpans.Second, + startAfter: TimeSpans.TenSeconds); + } + + /// + public void StopMining() + { + this.miningCancellationTokenSource.Cancel(); + this.miningLoop?.Dispose(); + this.miningLoop = null; + this.miningCancellationTokenSource.Dispose(); + this.miningCancellationTokenSource = null; + } + + /// + public List GenerateBlocks(ReserveScript reserveScript, ulong amountOfBlocksToMine, ulong maxTries) + { + var context = new MineBlockContext(amountOfBlocksToMine, (ulong)this.chainIndexer.Height, maxTries, reserveScript); + + while (context.MiningCanContinue) + { + if (!this.ConsensusIsAtTip(context)) + continue; + + if (!this.BuildBlock(context)) + continue; + + if (!this.IsProofOfWorkAllowed(context)) + continue; + + if (!this.MineBlock(context)) + break; + + if (!this.ValidateMinedBlock(context)) + continue; + + if (!this.ValidateAndConnectBlock(context)) + continue; + + this.OnBlockMined(context); + } + + return context.Blocks; + } + + private bool MineBlock(MineBlockContext context) + { + if (this.network.NetworkType == NetworkType.Regtest) + return MineBlockRegTest(context); + + if (this.minerSettings.UseOpenCL && this.openCLMiner.CanMine()) + return MineBlockOpenCL(context); + + return MineBlockCpu(context); + } + + private bool MineBlockCpu(MineBlockContext context) + { + context.ExtraNonce = IncrementExtraNonce(context.BlockTemplate.Block, context.ChainTip, context.ExtraNonce); + + Block block = context.BlockTemplate.Block; + block.Header.Nonce = 0; + + uint loopLength = 2_000_000; + int threads = Math.Max(1, Math.Min(this.minerSettings.MineThreadCount, Environment.ProcessorCount)); + + int batch = threads; + var totalNonce = batch * loopLength; + uint winnerNonce = 0; + bool found = false; + + ParallelOptions options = new ParallelOptions { MaxDegreeOfParallelism = threads, CancellationToken = this.miningCancellationTokenSource.Token }; + + + this.stopwatch.Restart(); + + int fromInclusive = context.ExtraNonce * batch; + int toExclusive = fromInclusive + batch; + + Parallel.For(fromInclusive, toExclusive, options, (index, state) => + { + if (this.miningCancellationTokenSource.Token.IsCancellationRequested) + return; + + uint256 bits = block.Header.Bits.ToUInt256(); + + var headerBytes = block.Header.ToBytes(this.network.Consensus.ConsensusFactory); + uint nonce = (uint)index * loopLength; + + var end = nonce + loopLength; + + while (nonce < end) + { + if (CheckProofOfWork(headerBytes, nonce, bits)) + { + winnerNonce = nonce; + found = true; + state.Stop(); + + return; + } + + if (state.IsStopped) + return; + + ++nonce; + } + }); + + if (found) + { + block.Header.Nonce = winnerNonce; + if (block.Header.CheckProofOfWork()) + return true; + } + + this.LogMiningInformation(context.ExtraNonce, totalNonce, this.stopwatch.Elapsed.TotalSeconds, block.Header.Bits.Difficulty, $"{threads} threads"); + + return false; + } + + private bool MineBlockOpenCL(MineBlockContext context) + { + Block block = context.BlockTemplate.Block; + block.Header.Nonce = 0; + context.ExtraNonce = this.IncrementExtraNonce(block, context.ChainTip, context.ExtraNonce); + + var iterations = uint.MaxValue / (uint)this.minerSettings.OpenCLWorksizeSplit; + var nonceStart = ((uint)context.ExtraNonce - 1) * iterations; + + + this.stopwatch.Restart(); + + var headerBytes = block.Header.ToBytes(this.network.Consensus.ConsensusFactory); + uint256 bits = block.Header.Bits.ToUInt256(); + var foundNonce = this.openCLMiner.FindPow(headerBytes, bits.ToBytes(), nonceStart, iterations); + + + if (foundNonce > 0) + { + block.Header.Nonce = foundNonce; + if (block.Header.CheckProofOfWork()) + { + return true; + } + } + + this.LogMiningInformation(context.ExtraNonce, iterations, this.stopwatch.Elapsed.TotalSeconds, block.Header.Bits.Difficulty, $"{this.openCLMiner.GetDeviceName()}"); + + if (context.ExtraNonce >= this.minerSettings.OpenCLWorksizeSplit) + { + block.Header.Time += 1; + context.ExtraNonce = 0; + } + + return false; + } + + private void LogMiningInformation(int extraNonce, long totalHashes, double totalSeconds, double difficultly, string minerInfo) + { + var MHashedPerSec = Math.Round((totalHashes / totalSeconds) / 1_000_000, 4); + + var currentDifficulty = BigInteger.ValueOf((long)difficultly); + var MHashedPerSecTotal = currentDifficulty.Multiply(this.pow256) + .Divide(Target.Difficulty1.ToBigInteger()).Divide(BigInteger.ValueOf(10 * 60)) + .LongValue / 1_000_000.0; + + this.logger.LogInformation($"Difficulty={difficultly}, extraNonce={extraNonce}, " + + $"hashes={totalHashes}, execution={totalSeconds} sec, " + + $"hash-rate={MHashedPerSec} MHash/sec ({minerInfo}), " + + $"network hash-rate ~{MHashedPerSecTotal} MHash/sec"); + } + + private static bool CheckProofOfWork(byte[] header, uint nonce, uint256 bits) + { + var bytes = BitConverter.GetBytes(nonce); + header[76] = bytes[0]; + header[77] = bytes[1]; + header[78] = bytes[2]; + header[79] = bytes[3]; + + uint256 headerHash = Sha512T.GetHash(header); + + var res = headerHash <= bits; + + return res; + } + + private bool IsProofOfWorkAllowed(MineBlockContext context) + { + var newBlockHeight = context.ChainTip.Height + 1; + + if (this.network.Consensus.Options is X1ConsensusOptions options) + { + if (options.IsAlgorithmAllowed(false, newBlockHeight)) + return true; + + Task.Delay(1000).Wait(); // pause the miner + return false; + } + + return true; + } + + + /// + /// Ensures that the node is synced before mining is allowed to start. + /// + private bool ConsensusIsAtTip(MineBlockContext context) + { + this.miningCancellationTokenSource.Token.ThrowIfCancellationRequested(); + + context.ChainTip = this.consensusManager.Tip; + + // Genesis on a regtest network is a special case. We need to regard ourselves as outside of IBD to + // bootstrap the mining. + if (context.ChainTip.Height == 0) + return true; + + if (this.initialBlockDownloadState.IsInitialBlockDownload()) + { + Task.Delay(TimeSpan.FromMinutes(1), this.nodeLifetime.ApplicationStopping).GetAwaiter().GetResult(); + return false; + } + + return true; + } + + /// + /// Creates a proof of work or proof of stake block depending on the network the node is running on. + /// + /// If the node is on a POS network, make sure the POS consensus rules are valid. This is required for + /// generation of blocks inside tests, where it is possible to generate multiple blocks within one second. + /// + /// + private bool BuildBlock(MineBlockContext context) + { + context.BlockTemplate = this.blockProvider.BuildPowBlock(context.ChainTip, context.ReserveScript.ReserveFullNodeScript); + + if (this.network.Consensus.IsProofOfStake) + { + if (context.BlockTemplate.Block.Header.Time <= context.ChainTip.Header.Time) + return false; + } + + return true; + } + + /// + /// Executes until the required work (difficulty) has been reached. This is the "mining" process. + /// + private bool MineBlockRegTest(MineBlockContext context) + { + context.ExtraNonce = this.IncrementExtraNonce(context.BlockTemplate.Block, context.ChainTip, context.ExtraNonce); + + Block block = context.BlockTemplate.Block; + while ((context.MaxTries > 0) && (block.Header.Nonce < InnerLoopCount) && !block.CheckProofOfWork()) + { + this.miningCancellationTokenSource.Token.ThrowIfCancellationRequested(); + + ++block.Header.Nonce; + --context.MaxTries; + } + + if (context.MaxTries == 0) + return false; + + return true; + } + + /// + /// Ensures that the block was properly mined by checking the block's work against the next difficulty target. + /// + private bool ValidateMinedBlock(MineBlockContext context) + { + if (context.BlockTemplate.Block.Header.Nonce == InnerLoopCount) + return false; + + var chainedHeader = new ChainedHeader(context.BlockTemplate.Block.Header, context.BlockTemplate.Block.GetHash(), context.ChainTip); + if (chainedHeader.ChainWork <= context.ChainTip.ChainWork) + return false; + + return true; + } + + /// + /// Validate the mined block by passing it to the consensus rule engine. + /// + /// On successful block validation the block will be connected to the chain. + /// + /// + private bool ValidateAndConnectBlock(MineBlockContext context) + { + ChainedHeader chainedHeader = this.consensusManager.BlockMinedAsync(context.BlockTemplate.Block).GetAwaiter().GetResult(); + + if (chainedHeader == null) + { + this.logger.LogTrace("(-)[BLOCK_VALIDATION_ERROR]:false"); + return false; + } + + context.ChainedHeaderBlock = new ChainedHeaderBlock(context.BlockTemplate.Block, chainedHeader); + + return true; + } + + private void OnBlockMined(MineBlockContext context) + { + this.logger.LogInformation("Mined new {0} block: '{1}'.", BlockStake.IsProofOfStake(context.ChainedHeaderBlock.Block) ? "POS" : "POW", context.ChainedHeaderBlock.ChainedHeader); + + context.CurrentHeight++; + + // memory leak...? + context.Blocks.Add(context.BlockTemplate.Block.GetHash()); + context.BlockTemplate = null; + } + + // + public int IncrementExtraNonce(Block block, ChainedHeader previousHeader, int extraNonce) + { + if (this.hashPrevBlock != block.Header.HashPrevBlock) + { + extraNonce = 0; // when the previous block changes, start extraNonce with 0 + this.hashPrevBlock = block.Header.HashPrevBlock; + } + + // BIP34 requires the coinbase first input to start with the block height. + int height = previousHeader.Height + 1; + + // Bitcoin Core appends the height and extra nonce in the following way: + // txCoinbase.vin[0].scriptSig = (CScript() << nHeight << CScriptNum(nExtraNonce)); + var heightScriptBytes = new Script(Op.GetPushOp(height)).ToBytes(); + var extraNonceScriptBytes = new CScriptNum(extraNonce).getvch(); + var scriptSigBytes = new byte[heightScriptBytes.Length + extraNonceScriptBytes.Length]; + Buffer.BlockCopy(heightScriptBytes, 0, scriptSigBytes, 0, heightScriptBytes.Length); + Buffer.BlockCopy(extraNonceScriptBytes, 0, scriptSigBytes, heightScriptBytes.Length, extraNonceScriptBytes.Length); + + block.Transactions[0].Inputs[0].ScriptSig = new Script(scriptSigBytes); + + this.blockProvider.BlockModified(previousHeader, block); + + Guard.Assert(block.Transactions[0].Inputs[0].ScriptSig.Length <= 100); + + return ++extraNonce; // increment and return new value + } + + /// + /// Context class that holds information on the current state of the mining process (per block). + /// + private class MineBlockContext + { + private readonly ulong amountOfBlocksToMine; + public List Blocks = new List(); + public BlockTemplate BlockTemplate { get; set; } + public ulong ChainHeight { get; set; } + public ChainedHeaderBlock ChainedHeaderBlock { get; internal set; } + public ulong CurrentHeight { get; set; } + public ChainedHeader ChainTip { get; set; } + public int ExtraNonce { get; set; } + public ulong MaxTries { get; set; } + public bool MiningCanContinue { get { return this.CurrentHeight < this.ChainHeight + this.amountOfBlocksToMine; } } + public readonly ReserveScript ReserveScript; + + public MineBlockContext(ulong amountOfBlocksToMine, ulong chainHeight, ulong maxTries, ReserveScript reserveScript) + { + this.amountOfBlocksToMine = amountOfBlocksToMine; + this.ChainHeight = chainHeight; + this.CurrentHeight = chainHeight; + this.MaxTries = maxTries; + this.ReserveScript = reserveScript; + } + } + + /// + /// CScriptNum implementation, taken from NBitcoin. + /// + public class CScriptNum + { + private const long nMaxNumSize = 4; + /** + * Numeric opcodes (OP_1ADD, etc) are restricted to operating on 4-byte integers. + * The semantics are subtle, though: operands must be in the range [-2^31 +1...2^31 -1], + * but results may overflow (and are valid as long as they are not used in a subsequent + * numeric operation). CScriptNum enforces those semantics by storing results as + * an int64 and allowing out-of-range values to be returned as a vector of bytes but + * throwing an exception if arithmetic is done or the result is interpreted as an integer. + */ + + public CScriptNum(long n) + { + this.m_value = n; + } + private long m_value; + + public CScriptNum(byte[] vch, bool fRequireMinimal) + : this(vch, fRequireMinimal, 4) + { + + } + public CScriptNum(byte[] vch, bool fRequireMinimal, long nMaxNumSize) + { + if (vch.Length > nMaxNumSize) + { + throw new ArgumentException("script number overflow", nameof(vch)); + } + if (fRequireMinimal && vch.Length > 0) + { + // Check that the number is encoded with the minimum possible + // number of bytes. + // + // If the most-significant-byte - excluding the sign bit - is zero + // then we're not minimal. Note how this test also rejects the + // negative-zero encoding, 0x80. + if ((vch[vch.Length - 1] & 0x7f) == 0) + { + // One exception: if there's more than one byte and the most + // significant bit of the second-most-significant-byte is set + // it would conflict with the sign bit. An example of this case + // is +-255, which encode to 0xff00 and 0xff80 respectively. + // (big-endian). + if (vch.Length <= 1 || (vch[vch.Length - 2] & 0x80) == 0) + { + throw new ArgumentException("non-minimally encoded script number", nameof(vch)); + } + } + } + + this.m_value = set_vch(vch); + } + + public override int GetHashCode() + { + return getint(); + } + public override bool Equals(object obj) + { + if (!(obj is CScriptNum)) + return false; + var item = (CScriptNum)obj; + return this.m_value == item.m_value; + } + public static bool operator ==(CScriptNum num, long rhs) + { + return num.m_value == rhs; + } + public static bool operator !=(CScriptNum num, long rhs) + { + return num.m_value != rhs; + } + public static bool operator <=(CScriptNum num, long rhs) + { + return num.m_value <= rhs; + } + public static bool operator <(CScriptNum num, long rhs) + { + return num.m_value < rhs; + } + public static bool operator >=(CScriptNum num, long rhs) + { + return num.m_value >= rhs; + } + public static bool operator >(CScriptNum num, long rhs) + { + return num.m_value > rhs; + } + + public static bool operator ==(CScriptNum a, CScriptNum b) + { + return a.m_value == b.m_value; + } + public static bool operator !=(CScriptNum a, CScriptNum b) + { + return a.m_value != b.m_value; + } + public static bool operator <=(CScriptNum a, CScriptNum b) + { + return a.m_value <= b.m_value; + } + public static bool operator <(CScriptNum a, CScriptNum b) + { + return a.m_value < b.m_value; + } + public static bool operator >=(CScriptNum a, CScriptNum b) + { + return a.m_value >= b.m_value; + } + public static bool operator >(CScriptNum a, CScriptNum b) + { + return a.m_value > b.m_value; + } + + public static CScriptNum operator +(CScriptNum num, long rhs) + { + return new CScriptNum(num.m_value + rhs); + } + public static CScriptNum operator -(CScriptNum num, long rhs) + { + return new CScriptNum(num.m_value - rhs); + } + public static CScriptNum operator +(CScriptNum a, CScriptNum b) + { + return new CScriptNum(a.m_value + b.m_value); + } + public static CScriptNum operator -(CScriptNum a, CScriptNum b) + { + return new CScriptNum(a.m_value - b.m_value); + } + + public static CScriptNum operator &(CScriptNum a, long b) + { + return new CScriptNum(a.m_value & b); + } + public static CScriptNum operator &(CScriptNum a, CScriptNum b) + { + return new CScriptNum(a.m_value & b.m_value); + } + + + + public static CScriptNum operator -(CScriptNum num) + { + assert(num.m_value != long.MinValue); + return new CScriptNum(-num.m_value); + } + + private static void assert(bool result) + { + if (!result) + throw new InvalidOperationException("Assertion fail for CScriptNum"); + } + + public static implicit operator CScriptNum(long rhs) + { + return new CScriptNum(rhs); + } + + public static explicit operator long(CScriptNum rhs) + { + return rhs.m_value; + } + + public static explicit operator uint(CScriptNum rhs) + { + return (uint)rhs.m_value; + } + + + + public int getint() + { + if (this.m_value > int.MaxValue) + return int.MaxValue; + else if (this.m_value < int.MinValue) + return int.MinValue; + return (int)this.m_value; + } + + public byte[] getvch() + { + return serialize(this.m_value); + } + + private static byte[] serialize(long value) + { + if (value == 0) + return new byte[0]; + + var result = new List(); + bool neg = value < 0; + long absvalue = neg ? -value : value; + + while (absvalue != 0) + { + result.Add((byte)(absvalue & 0xff)); + absvalue >>= 8; + } + + // - If the most significant byte is >= 0x80 and the value is positive, push a + // new zero-byte to make the significant byte < 0x80 again. + + // - If the most significant byte is >= 0x80 and the value is negative, push a + // new 0x80 byte that will be popped off when converting to an integral. + + // - If the most significant byte is < 0x80 and the value is negative, add + // 0x80 to it, since it will be subtracted and interpreted as a negative when + // converting to an integral. + + if ((result[result.Count - 1] & 0x80) != 0) + result.Add((byte)(neg ? 0x80 : 0)); + else if (neg) + result[result.Count - 1] |= 0x80; + + return result.ToArray(); + } + + private static long set_vch(byte[] vch) + { + if (vch.Length == 0) + return 0; + + long result = 0; + for (int i = 0; i != vch.Length; ++i) + result |= ((long)(vch[i])) << 8 * i; + + // If the input vector's most significant byte is 0x80, remove it from + // the result's msb and return a negative. + if ((vch[vch.Length - 1] & 0x80) != 0) + { + ulong temp = ~(0x80UL << (8 * (vch.Length - 1))); + return -((long)((ulong)result & temp)); + } + + return result; + } + } + } +} diff --git a/src/Networks/Blockcore.Networks.X1/Components/X1StakeValidator.cs b/src/Networks/Blockcore.Networks.X1/Components/X1StakeValidator.cs new file mode 100644 index 000000000..ef77e1833 --- /dev/null +++ b/src/Networks/Blockcore.Networks.X1/Components/X1StakeValidator.cs @@ -0,0 +1,466 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Blockcore.Consensus; +using Blockcore.Consensus.BlockInfo; +using Blockcore.Consensus.Chain; +using Blockcore.Consensus.ScriptInfo; +using Blockcore.Consensus.TransactionInfo; +using Blockcore.Features.Consensus; +using Blockcore.Features.Consensus.CoinViews; +using Blockcore.Features.Consensus.Interfaces; +using Blockcore.Networks.X1.Consensus; +using Blockcore.Utilities; +using Microsoft.Extensions.Logging; +using NBitcoin; +using NBitcoin.BouncyCastle.Math; +using NBitcoin.Crypto; + +namespace Blockcore.Networks.X1.Components +{ + public class X1StakeValidator : IStakeValidator + { + /// When checking the POS block signature this determines the maximum push data (public key) size following the OP_RETURN in the nonspendable output. + private const int MaxPushDataSize = 40; + + // TODO: move this to IConsensus + /// Time interval in minutes that is used in the retarget calculation. + private const uint RetargetIntervalMinutes = 16; + + /// Instance logger. + private readonly ILogger logger; + + /// Database of stake related data for the current blockchain. + private readonly IStakeChain stakeChain; + + /// Thread safe access to the best chain of block headers (that the node is aware of) from genesis. + private readonly ChainIndexer chainIndexer; + + /// Consensus' view of UTXO set. + private readonly ICoinView coinView; + + /// + private readonly Network network; + + /// + /// Specification of the network the node runs on - regtest/testnet/mainnet. + /// Database of stake related data for the current blockchain. + /// Chain of headers. + /// Used for getting UTXOs. + /// Factory for creating loggers. + public X1StakeValidator(Network network, IStakeChain stakeChain, ChainIndexer chainIndexer, ICoinView coinView, ILoggerFactory loggerFactory) + { + this.logger = loggerFactory.CreateLogger(this.GetType().FullName); + this.stakeChain = stakeChain; + this.chainIndexer = chainIndexer; + this.coinView = coinView; + this.network = network; + } + + /// + public ChainedHeader GetLastPowPosChainedBlock(IStakeChain stakeChain, ChainedHeader startChainedHeader, bool proofOfStake) + { + Guard.NotNull(stakeChain, nameof(stakeChain)); + Guard.Assert(startChainedHeader != null); + + BlockStake blockStake = stakeChain.Get(startChainedHeader.HashBlock); + + while ((startChainedHeader.Previous != null) && (blockStake.IsProofOfStake() != proofOfStake)) + { + startChainedHeader = startChainedHeader.Previous; + blockStake = stakeChain.Get(startChainedHeader.HashBlock); + } + + return startChainedHeader; + } + + /// + public Target CalculateRetarget(uint firstBlockTime, Target firstBlockTarget, uint secondBlockTime, BigInteger targetLimit) + { + uint targetSpacing = (uint)this.network.Consensus.TargetSpacing.TotalSeconds; + uint actualSpacing = firstBlockTime > secondBlockTime ? firstBlockTime - secondBlockTime : targetSpacing; + + if (actualSpacing > targetSpacing * 10) + actualSpacing = targetSpacing * 10; + + uint targetTimespan = RetargetIntervalMinutes * 60; + uint interval = targetTimespan / targetSpacing; + + BigInteger target = firstBlockTarget.ToBigInteger(); + + long multiplyBy = (interval - 1) * targetSpacing + actualSpacing + actualSpacing; + target = target.Multiply(BigInteger.ValueOf(multiplyBy)); + + long divideBy = (interval + 1) * targetSpacing; + target = target.Divide(BigInteger.ValueOf(divideBy)); + + this.logger.LogDebug("The next target difficulty will be {0} times higher (easier to satisfy) than the previous target.", (double)multiplyBy / (double)divideBy); + + if ((target.CompareTo(BigInteger.Zero) <= 0) || (target.CompareTo(targetLimit) >= 1)) + target = targetLimit; + + var finalTarget = new Target(target); + + return finalTarget; + } + + /// + public Target GetNextTargetRequired(IStakeChain stakeChain, ChainedHeader chainTip, IConsensus consensus, bool proofOfStake) + { + Guard.NotNull(stakeChain, nameof(stakeChain)); + + // If the chain uses a PosPowRatchet, we branch away here, 4 blocks after it has activated. A safe delta of 4 + // is used, so that when we iterate over blocks backwards, we'll never hit non-Ratchet blocks. + if (consensus.Options is X1ConsensusOptions options && + options.IsPosPowRatchetActiveAtHeight(chainTip.Height - 4)) + { + bool isChainTipProofOfStake = stakeChain.Get(chainTip.HashBlock).IsProofOfStake(); + if (isChainTipProofOfStake && chainTip.Height % 2 != 0 || !isChainTipProofOfStake && chainTip.Height % 2 == 0) + throw new InvalidOperationException("Misconfiguration: When the ratchet is active for a height, the convention that PoS block heights are even numbers, must be met."); + + return options.GetNextTargetRequired(chainTip, isChainTipProofOfStake, consensus, proofOfStake); + } + + + // Genesis block. + if (chainTip == null) + { + this.logger.LogTrace("(-)[GENESIS]:'{0}'", consensus.PowLimit); + return consensus.PowLimit; + } + + // Find the last two blocks that correspond to the mining algo + // (i.e if this is a POS block we need to find the last two POS blocks). + BigInteger targetLimit = proofOfStake + ? consensus.ProofOfStakeLimitV2 + : consensus.PowLimit.ToBigInteger(); + + // First block. + ChainedHeader lastPowPosBlock = this.GetLastPowPosChainedBlock(stakeChain, chainTip, proofOfStake); + if (lastPowPosBlock.Previous == null) + { + var res = new Target(targetLimit); + this.logger.LogTrace("(-)[FIRST_BLOCK]:'{0}'", res); + return res; + } + + // Second block. + ChainedHeader prevLastPowPosBlock = this.GetLastPowPosChainedBlock(stakeChain, lastPowPosBlock.Previous, proofOfStake); + if (prevLastPowPosBlock.Previous == null) + { + var res = new Target(targetLimit); + this.logger.LogTrace("(-)[SECOND_BLOCK]:'{0}'", res); + return res; + } + + // This is used in tests to allow quickly mining blocks. + if (!proofOfStake && consensus.PowNoRetargeting) + { + this.logger.LogTrace("(-)[NO_POW_RETARGET]:'{0}'", lastPowPosBlock.Header.Bits); + return lastPowPosBlock.Header.Bits; + } + + if (proofOfStake && consensus.PosNoRetargeting) + { + this.logger.LogTrace("(-)[NO_POS_RETARGET]:'{0}'", lastPowPosBlock.Header.Bits); + return lastPowPosBlock.Header.Bits; + } + + Target finalTarget = this.CalculateRetarget(lastPowPosBlock.Header.Time, lastPowPosBlock.Header.Bits, prevLastPowPosBlock.Header.Time, targetLimit); + + return finalTarget; + } + + /// + public void CheckProofOfStake(PosRuleContext context, ChainedHeader prevChainedHeader, BlockStake prevBlockStake, Transaction transaction, uint headerBits) + { + Guard.NotNull(context, nameof(context)); + Guard.NotNull(prevChainedHeader, nameof(prevChainedHeader)); + Guard.NotNull(prevBlockStake, nameof(prevBlockStake)); + Guard.NotNull(transaction, nameof(transaction)); + + if (!transaction.IsCoinStake) + { + this.logger.LogTrace("(-)[NO_COINSTAKE]"); + ConsensusErrors.NonCoinstake.Throw(); + } + + TxIn txIn = transaction.Inputs[0]; + + UnspentOutput prevUtxo = context.UnspentOutputSet.AccessCoins(txIn.PrevOut); + if (prevUtxo == null) + { + this.logger.LogTrace("(-)[PREV_UTXO_IS_NULL]"); + ConsensusErrors.ReadTxPrevFailed.Throw(); + } + + // Verify signature. + if (!this.VerifySignature(prevUtxo, transaction, 0, ScriptVerify.None)) + { + this.logger.LogTrace("(-)[BAD_SIGNATURE]"); + ConsensusErrors.CoinstakeVerifySignatureFailed.Throw(); + } + + // Min age requirement. + if (this.IsConfirmedInNPrevBlocks(prevUtxo, prevChainedHeader, this.GetTargetDepthRequired(prevChainedHeader))) + { + this.logger.LogTrace("(-)[BAD_STAKE_DEPTH]"); + ConsensusErrors.InvalidStakeDepth.Throw(); + } + + if (!this.CheckStakeKernelHash(context, headerBits, prevBlockStake.StakeModifierV2, prevUtxo, txIn.PrevOut, context.ValidationContext.ChainedHeaderToValidate.Header.Time)) + { + this.logger.LogTrace("(-)[INVALID_STAKE_HASH_TARGET]"); + ConsensusErrors.StakeHashInvalidTarget.Throw(); + } + } + + /// + public uint256 ComputeStakeModifierV2(ChainedHeader prevChainedHeader, uint256 prevStakeModifier, uint256 kernel) + { + Guard.NotNull(prevStakeModifier, nameof(prevStakeModifier)); + if (prevChainedHeader == null) + return 0; // Genesis block's modifier is 0. + + uint256 stakeModifier; + using (var ms = new MemoryStream()) + { + var serializer = new BitcoinStream(ms, true); + serializer.ReadWrite(kernel); + serializer.ReadWrite(prevStakeModifier); + stakeModifier = Hashes.Hash256(ms.ToArray()); + } + + return stakeModifier; + } + + /// + public bool CheckKernel(PosRuleContext context, ChainedHeader prevChainedHeader, uint headerBits, long transactionTime, OutPoint prevout) + { + Guard.NotNull(context, nameof(context)); + Guard.NotNull(prevout, nameof(prevout)); + Guard.NotNull(prevChainedHeader, nameof(prevChainedHeader)); + + FetchCoinsResponse coins = this.coinView.FetchCoins(new[] { prevout }); + if ((coins == null) || (coins.UnspentOutputs.Count != 1)) + { + this.logger.LogTrace("(-)[READ_PREV_TX_FAILED]"); + ConsensusErrors.ReadTxPrevFailed.Throw(); + } + + ChainedHeader prevBlock = this.chainIndexer.GetHeader(this.coinView.GetTipHash().Hash); + if (prevBlock == null) + { + this.logger.LogTrace("(-)[REORG]"); + ConsensusErrors.ReadTxPrevFailed.Throw(); + } + + UnspentOutput prevUtxo = coins.UnspentOutputs.Single().Value; + if (prevUtxo == null) + { + this.logger.LogTrace("(-)[PREV_UTXO_IS_NULL]"); + ConsensusErrors.ReadTxPrevFailed.Throw(); + } + + if (this.IsConfirmedInNPrevBlocks(prevUtxo, prevChainedHeader, this.GetTargetDepthRequired(prevChainedHeader))) + { + this.logger.LogTrace("(-)[LOW_COIN_AGE]"); + ConsensusErrors.InvalidStakeDepth.Throw(); + } + + BlockStake prevBlockStake = this.stakeChain.Get(prevChainedHeader.HashBlock); + if (prevBlockStake == null) + { + this.logger.LogTrace("(-)[BAD_STAKE_BLOCK]"); + ConsensusErrors.BadStakeBlock.Throw(); + } + + return this.CheckStakeKernelHash(context, headerBits, prevBlockStake.StakeModifierV2, prevUtxo, prevout, (uint)transactionTime); + } + + /// + public bool CheckStakeKernelHash(PosRuleContext context, uint headerBits, uint256 prevStakeModifier, UnspentOutput stakingCoins, OutPoint prevout, uint transactionTime) + { + Guard.NotNull(context, nameof(context)); + Guard.NotNull(prevout, nameof(prevout)); + Guard.NotNull(stakingCoins, nameof(stakingCoins)); + + if (transactionTime < stakingCoins.Coins.Time) + { + this.logger.LogDebug("Coinstake transaction timestamp {0} is lower than it's own UTXO timestamp {1}.", transactionTime, stakingCoins.Coins.Time); + this.logger.LogTrace("(-)[BAD_STAKE_TIME]"); + ConsensusErrors.StakeTimeViolation.Throw(); + } + + // Base target. + BigInteger target = new Target(headerBits).ToBigInteger(); + + // Weighted target. + long valueIn = stakingCoins.Coins.TxOut.Value.Satoshi; + BigInteger weight = BigInteger.ValueOf(valueIn); + BigInteger weightedTarget = target.Multiply(weight); + + context.TargetProofOfStake = this.ToUInt256(weightedTarget); + this.logger.LogDebug("POS target is '{0}', weighted target for {1} coins is '{2}'.", this.ToUInt256(target), valueIn, context.TargetProofOfStake); + + // Calculate hash. + using (var ms = new MemoryStream()) + { + var serializer = new BitcoinStream(ms, true); + serializer.ReadWrite(prevStakeModifier); + if (this.network.Consensus.PosUseTimeFieldInKernalHash) // old posv3 time field + serializer.ReadWrite(stakingCoins.Coins.Time); + serializer.ReadWrite(prevout.Hash); + serializer.ReadWrite(prevout.N); + serializer.ReadWrite(transactionTime); + + context.HashProofOfStake = Hashes.Hash256(ms.ToArray()); + } + + this.logger.LogDebug("Stake modifier V2 is '{0}', hash POS is '{1}'.", prevStakeModifier, context.HashProofOfStake); + + // Now check if proof-of-stake hash meets target protocol. + var hashProofOfStakeTarget = new BigInteger(1, context.HashProofOfStake.ToBytes(false)); + if (hashProofOfStakeTarget.CompareTo(weightedTarget) > 0) + { + this.logger.LogTrace("(-)[TARGET_MISSED]"); + return false; + } + + return true; + } + + /// + public bool VerifySignature(UnspentOutput coin, Transaction txTo, int txToInN, ScriptVerify flagScriptVerify) + { + Guard.NotNull(coin, nameof(coin)); + Guard.NotNull(txTo, nameof(txTo)); + + if (txToInN < 0 || txToInN >= txTo.Inputs.Count) + return false; + + TxIn input = txTo.Inputs[txToInN]; + + if (input.PrevOut.Hash != coin.OutPoint.Hash) + { + this.logger.LogTrace("(-)[INCORRECT_TX]"); + return false; + } + + TxOut output = coin.Coins.TxOut;//.Outputs[input.PrevOut.N]; + + if (output == null) + { + this.logger.LogTrace("(-)[OUTPUT_NOT_FOUND]"); + return false; + } + + var txData = new PrecomputedTransactionData(txTo); + var checker = new TransactionChecker(txTo, txToInN, output.Value, txData); + var ctx = new ScriptEvaluationContext(this.chainIndexer.Network) { ScriptVerify = flagScriptVerify }; + + bool res = ctx.VerifyScript(input.ScriptSig, output.ScriptPubKey, checker); + return res; + } + + /// + public bool IsConfirmedInNPrevBlocks(UnspentOutput coins, ChainedHeader referenceChainedHeader, long targetDepth) + { + Guard.NotNull(coins, nameof(coins)); + Guard.NotNull(referenceChainedHeader, nameof(referenceChainedHeader)); + + int actualDepth = referenceChainedHeader.Height - (int)coins.Coins.Height; + bool res = actualDepth < targetDepth; + + return res; + } + + /// + public long GetTargetDepthRequired(ChainedHeader prevChainedHeader) + { + Guard.NotNull(prevChainedHeader, nameof(ChainedHeader)); + + return ((X1ConsensusOptions) this.network.Consensus.Options).GetStakeMinConfirmations(prevChainedHeader.Height + 1, this.network) - 1; + } + + /// + /// Converts to . + /// + /// input value. + /// version of . + private uint256 ToUInt256(BigInteger input) + { + byte[] array = input.ToByteArray(); + + int missingZero = 32 - array.Length; + + if (missingZero < 0) + return new uint256(array.Skip(Math.Abs(missingZero)).ToArray(), false); + + if (missingZero > 0) + return new uint256(new byte[missingZero].Concat(array).ToArray(), false); + + return new uint256(array, false); + } + + /// + public bool CheckStakeSignature(BlockSignature signature, uint256 blockHash, Transaction coinStake) + { + if (signature.IsEmpty()) + { + this.logger.LogTrace("(-)[EMPTY]:false"); + return false; + } + + TxOut txout = coinStake.Outputs[1]; + + if (PayToPubkeyTemplate.Instance.CheckScriptPubKey(txout.ScriptPubKey)) + { + PubKey pubKey = PayToPubkeyTemplate.Instance.ExtractScriptPubKeyParameters(txout.ScriptPubKey); + bool res = pubKey.Verify(blockHash, new ECDSASignature(signature.Signature)); + this.logger.LogTrace("(-)[P2PK]:{0}", res); + return res; + } + + // Block signing key also can be encoded in the nonspendable output. + // This allows to not pollute UTXO set with useless outputs e.g. in case of multisig staking. + + List ops = txout.ScriptPubKey.ToOps().ToList(); + if (!ops.Any()) + { + this.logger.LogTrace("(-)[NO_OPS]:false"); + return false; + } + + if (ops.ElementAt(0).Code != OpcodeType.OP_RETURN) // OP_RETURN) + { + this.logger.LogTrace("(-)[NO_OP_RETURN]:false"); + return false; + } + + if (ops.Count != 2) + { + this.logger.LogTrace("(-)[INVALID_OP_COUNT]:false"); + return false; + } + + byte[] data = ops.ElementAt(1).PushData; + + if (data.Length > MaxPushDataSize) + { + this.logger.LogTrace("(-)[PUSH_DATA_TOO_LARGE]:false"); + return false; + } + + if (!ScriptEvaluationContext.IsCompressedOrUncompressedPubKey(data)) + { + this.logger.LogTrace("(-)[NO_PUSH_DATA]:false"); + return false; + } + + bool verifyRes = new PubKey(data).Verify(blockHash, new ECDSASignature(signature.Signature)); + return verifyRes; + } + } +} diff --git a/src/Networks/Blockcore.Networks.X1/Consensus/X1BlockHeader.cs b/src/Networks/Blockcore.Networks.X1/Consensus/X1BlockHeader.cs new file mode 100644 index 000000000..e2a50ad31 --- /dev/null +++ b/src/Networks/Blockcore.Networks.X1/Consensus/X1BlockHeader.cs @@ -0,0 +1,23 @@ +using System.IO; +using Blockcore.Consensus.BlockInfo; +using NBitcoin; +using NBitcoin.Crypto; + +namespace Blockcore.Networks.X1.Consensus +{ + public class X1BlockHeader : PosBlockHeader + { + public override uint256 GetPoWHash() + { + byte[] serialized; + + using (var ms = new MemoryStream()) + { + this.ReadWriteHashingStream(new BitcoinStream(ms, true)); + serialized = ms.ToArray(); + } + + return Sha512T.GetHash(serialized); + } + } +} \ No newline at end of file diff --git a/src/Networks/Blockcore.Networks.X1/Consensus/X1ConsensusFactory.cs b/src/Networks/Blockcore.Networks.X1/Consensus/X1ConsensusFactory.cs new file mode 100644 index 000000000..2a89cff9d --- /dev/null +++ b/src/Networks/Blockcore.Networks.X1/Consensus/X1ConsensusFactory.cs @@ -0,0 +1,141 @@ +using System; +using System.Diagnostics; +using System.Text; +using System.Threading.Tasks; +using Blockcore.Consensus.BlockInfo; +using Blockcore.Consensus.ScriptInfo; +using Blockcore.Consensus.TransactionInfo; +using NBitcoin; +using NBitcoin.DataEncoders; + +namespace Blockcore.Networks.X1.Consensus +{ + public class X1ConsensusFactory : PosConsensusFactory + { + public override BlockHeader CreateBlockHeader() + { + return new X1BlockHeader(); + } + + public override ProvenBlockHeader CreateProvenBlockHeader() + { + return new X1ProvenBlockHeader(); + } + + public override ProvenBlockHeader CreateProvenBlockHeader(PosBlock block) + { + var provenBlockHeader = new X1ProvenBlockHeader(block, (X1BlockHeader)this.CreateBlockHeader()); + + // Serialize the size. + provenBlockHeader.ToBytes(this); + + return provenBlockHeader; + } + + public override Transaction CreateTransaction() + { + return new X1Transaction(); + } + + public override Transaction CreateTransaction(byte[] bytes) + { + if (bytes == null) + throw new ArgumentNullException(nameof(bytes)); + + var transaction = new X1Transaction(); + transaction.ReadWrite(bytes, this); + return transaction; + } + + public override Transaction CreateTransaction(string hex) + { + if (hex == null) + throw new ArgumentNullException(nameof(hex)); + + return CreateTransaction(Encoders.Hex.DecodeData(hex)); + } + + public Block ComputeGenesisBlock(uint genesisTime, uint genesisNonce, uint genesisBits, int genesisVersion, Money genesisReward, NetworkType networkType, bool? mine = false) + { + if (mine == true) + MineGenesisBlock(genesisTime, genesisBits, genesisVersion, genesisReward, networkType); + + string pszTimestamp = "https://www.blockchain.com/btc/block/611000"; + + Transaction txNew = CreateTransaction(); + Debug.Assert(txNew.GetType() == typeof(X1Transaction)); + + txNew.Version = 1; + + txNew.AddInput(new TxIn() + { + ScriptSig = new Script(Op.GetPushOp(0), new Op() + { + Code = (OpcodeType)0x1, + PushData = new[] { (byte)42 } + }, Op.GetPushOp(Encoding.UTF8.GetBytes(pszTimestamp))) + }); + txNew.AddOutput(new TxOut() + { + Value = genesisReward, + }); + Block genesis = CreateBlock(); + genesis.Header.BlockTime = Utils.UnixTimeToDateTime(genesisTime); + genesis.Header.Bits = genesisBits; + genesis.Header.Nonce = genesisNonce; + genesis.Header.Version = genesisVersion; + genesis.Transactions.Add(txNew); + genesis.Header.HashPrevBlock = uint256.Zero; + genesis.UpdateMerkleRoot(); + + if (mine == false) + { + switch (networkType) + { + case NetworkType.Mainnet: + if (genesis.GetHash() == + uint256.Parse("0000000e13c5bf36c155c7cb1681053d607c191fc44b863d0c5aef6d27b8eb8f") && + genesis.Header.HashMerkleRoot == + uint256.Parse("e3c549956232f0878414d765e83c3f9b1b084b0fa35643ddee62857220ea02b0")) + return genesis; + break; + case NetworkType.Testnet: + if (genesis.GetHash() == + uint256.Parse("00000d2ff9f3620b5487ed8ec154ce1947fec525e91e6973d1aeae93c53db7a3") && + genesis.Header.HashMerkleRoot == + uint256.Parse("e3c549956232f0878414d765e83c3f9b1b084b0fa35643ddee62857220ea02b0")) + return genesis; + break; + case NetworkType.Regtest: + if (genesis.GetHash() == + uint256.Parse("00000e48aeeedabface6d45c0de52c7d0edaec14662ab4f56401361f70d12cc6") && + genesis.Header.HashMerkleRoot == + uint256.Parse("e3c549956232f0878414d765e83c3f9b1b084b0fa35643ddee62857220ea02b0")) + return genesis; + break; + } + } + else if (mine == null) + return genesis; + + throw new InvalidOperationException($"Invalid {networkType}."); + } + + private void MineGenesisBlock(uint genesisTime, uint genesisBits, int genesisVersion, Money genesisReward, NetworkType networkType) + { + Parallel.ForEach(new long[] { 0, 1, 2, 3, 4, 5, 6, 7 }, l => + { + if (Utils.UnixTimeToDateTime(genesisTime) > DateTime.UtcNow) + throw new Exception("Time must not be in the future"); + uint nonce = 0; + while (!ComputeGenesisBlock(genesisTime, nonce, genesisBits, genesisVersion, genesisReward, networkType, null).GetHash().ToString().StartsWith("00000000")) + { + nonce += 8; + } + + Block genesisBlock = ComputeGenesisBlock(genesisTime, nonce, genesisBits, genesisVersion, genesisReward, networkType, null); + throw new Exception($"Found: Nonce:{nonce}, Hash: {genesisBlock.GetHash()}, Hash Merkle Root: {genesisBlock.Header.HashMerkleRoot}"); + }); + } + } +} \ No newline at end of file diff --git a/src/Networks/Blockcore.Networks.X1/Consensus/X1ConsensusOptions.cs b/src/Networks/Blockcore.Networks.X1/Consensus/X1ConsensusOptions.cs new file mode 100644 index 000000000..5a7172381 --- /dev/null +++ b/src/Networks/Blockcore.Networks.X1/Consensus/X1ConsensusOptions.cs @@ -0,0 +1,347 @@ +using System; +using System.Runtime.CompilerServices; +using Blockcore.Consensus; +using Blockcore.Consensus.Chain; +using NBitcoin; +using NBitcoin.BouncyCastle.Math; + +namespace Blockcore.Networks.X1.Consensus +{ + /// + public class X1ConsensusOptions : PosConsensusOptions + { + private const int PosPowRatchetIsActiveHeightInvalid = -1; + + /// + /// The block height (inclusive), where the PosPowRatchet algorithm starts, on TestNet. + /// + private const int PosPowRatchetIsActiveHeightTestNet = 240; + + /// + /// The block height (inclusive), where the PosPowRatchet algorithm starts, on MainNet. + /// + private const int PosPowRatchetIsActiveHeightMainNet = 163300; // ~19/12/2020 04:00h + + private readonly Network currentNetwork; + + public X1ConsensusOptions(Network network) + { + this.currentNetwork = network; + } + + /// + public override int GetStakeMinConfirmations(int height, Network network) + { + // StakeMinConfirmations must equal MaxReorgLength so that nobody can stake in isolation and then force a reorg + return (int)network.Consensus.MaxReorgLength; + } + + /// + public bool IsAlgorithmAllowed(bool isProofOfStake, int newBlockHeight) + { + if (this.currentNetwork.NetworkType == NetworkType.Mainnet) + { + if (newBlockHeight < PosPowRatchetIsActiveHeightMainNet) + return true; + + bool isPosHeight = newBlockHeight % 2 == 0; // for X1, even block heights must be Proof-of-Stake + + if (isProofOfStake && isPosHeight) + return true; + + if (!isProofOfStake && !isPosHeight) + return true; + + return false; + } + + if (this.currentNetwork.NetworkType == NetworkType.Testnet) + { + if (newBlockHeight < PosPowRatchetIsActiveHeightTestNet) + return true; + + bool isPosHeight = newBlockHeight % 2 == 0; // for X1, even block heights must be Proof-of-Stake + + if (isProofOfStake && isPosHeight) + return true; + + if (!isProofOfStake && !isPosHeight) + return true; + + return false; + } + + return true; + } + + /// + public bool IsPosPowRatchetActiveAtHeight(int chainTipHeight) + { + if (this.currentNetwork.NetworkType == NetworkType.Testnet) + { + if (chainTipHeight >= PosPowRatchetIsActiveHeightTestNet) + return true; + } + if (this.currentNetwork.NetworkType == NetworkType.Mainnet) + { + if (chainTipHeight >= PosPowRatchetIsActiveHeightMainNet) + return true; + } + + return false; + } + + double GetTargetTimespanTotalSeconds(int height) + { + return GetTargetSpacingTotalSeconds(height) * 338; + } + + + double GetTargetSpacingTotalSeconds(int height) + { + if (this.currentNetwork.NetworkType != NetworkType.Mainnet) + return this.currentNetwork.Consensus.TargetSpacing.TotalSeconds; // 256 seconds + + // X1 Main + if (height < 165740) + return this.currentNetwork.Consensus.TargetSpacing.TotalSeconds; // 256 seconds + + return TimeSpan.FromMinutes(10).TotalSeconds; // 600 seconds + } + + + private readonly object lockObj = new object(); + + public Target GetNextTargetRequired(ChainedHeader currentChainTip, bool isChainTipProofOfStake, IConsensus consensus, bool isTargetRequestedForProofOfStake) + { + if (currentChainTip == null) + throw new ArgumentNullException(nameof(currentChainTip)); + + if (consensus == null) + throw new ArgumentNullException(nameof(consensus)); + + lock (this.lockObj) + { + // Precondition and sanity checks. Strict precondition checks here allow for more robust and faster code + // in the actual logic. Fast code is necessary here, because this code is called often, and it iterates + // over many headers to calculate the block interval averages. Therefore, the even/odd convention for PoS/PoW + // blocks is double-checked here as well, because it allows us not to use IStakeChain which would be slow when + // iterating over many headers. + + // This code must not be called before the ratchet has been active for at least 4 blocks. + if (!IsPosPowRatchetActiveAtHeight(currentChainTip.Height - 4)) + throw new InvalidOperationException($"Precondition failed: PosPowRatchet has not been active 4 blocks before the current tip height of {currentChainTip.Height}."); + + ChainedHeader lastPowPosBlock = currentChainTip; + + // The caller passes an argument, whether a PoS or PoW Target is requested. + if (isTargetRequestedForProofOfStake) + { + // Starting point will be the last PoS block. + if (!isChainTipProofOfStake) + { + // The previous block is guaranteed to be a PoS block, due to the offset of 2 to the ratchet activation height + // and the precondition check when calling this from StakeValidator. + lastPowPosBlock = lastPowPosBlock.Previous; + } + + // We are passing in a PoS block! + return GetNextPosTargetRequired(lastPowPosBlock, consensus); + } + + // Starting point will be the last PoW block. + if (isChainTipProofOfStake) + { + // The previous block is guaranteed to be a PoW block, due to the offset of 2 to the ratchet activation height + // and the precondition check when calling this from StakeValidator. + lastPowPosBlock = lastPowPosBlock.Previous; + } + + // We are passing in a PoW block! + return GetNextWorkRequired(lastPowPosBlock, consensus); + + } + } + + private Target GetNextWorkRequired(ChainedHeader lastPowBlock, IConsensus consensus) + { + int difficultyAdjustmentInterval = (int)(GetTargetTimespanTotalSeconds(lastPowBlock.Height) / GetTargetSpacingTotalSeconds(lastPowBlock.Height)); + + // Only change once per difficulty adjustment interval + if ((lastPowBlock.Height + 1) % difficultyAdjustmentInterval != 0) + { + if (consensus.PowAllowMinDifficultyBlocks) + { + // Special difficulty rule for testnet: + // If the new block's timestamp is more than 2 * TargetSpacing.TotalSeconds, + // then allow mining of a min-difficulty block. + if (lastPowBlock.Header.Time > lastPowBlock.Header.Time + GetTargetSpacingTotalSeconds(lastPowBlock.Height) * 2) + return consensus.PowLimit; + else + { + // Return the last non-special-min-difficulty-rules-block + ChainedHeader pindex = lastPowBlock; + while (pindex.Previous != null && pindex.Height % difficultyAdjustmentInterval != 0 && pindex.Header.Bits == consensus.PowLimit) + pindex = pindex.Previous; + return pindex.Header.Bits; + } + } + + // Not changing the Target means we return the previous PoW Target. + return lastPowBlock.Header.Bits; + } + + // We'll also not adjust the difficulty, if the ratchet wasn't active at least 2x difficultyAdjustmentInterval + 4 blocks. + if (lastPowBlock.Height < GetPosPowRatchetIsActiveHeight() + 2 * difficultyAdjustmentInterval + 4) + return lastPowBlock.Header.Bits; + + // Define the amount of PoW blocks used to calculate the average, and for the sake of logic, + // don't repeat Bitcoin's off-by one error. + var amountOfPoWBlocks = difficultyAdjustmentInterval; // 338 + + ChainedHeader powBlockIterator = lastPowBlock; + var powBlockCount = 0; + var posGaps = 0u; + var powIntervalsIncPosSum = 0u; + + while (powBlockCount < amountOfPoWBlocks) + { + + // we are sure this is a Pos block because of the precondition checks, which previous blocks have already passed + ChainedHeader intermediatePosBlock = powBlockIterator.Previous; + + // we are sure this is a Pow block because of the precondition checks, which previous blocks have already passed + ChainedHeader prevLastPowBlock = intermediatePosBlock.Previous; + + // the time in seconds the intermediate PoS block has used, which is must be hidden for the calculation + var posGapSeconds = intermediatePosBlock.Header.Time - prevLastPowBlock.Header.Time; + posGaps += posGapSeconds; + + var grossPowSeconds = powBlockIterator.Header.Time - prevLastPowBlock.Header.Time; + powIntervalsIncPosSum += grossPowSeconds; + + // update the iterator + powBlockIterator = prevLastPowBlock; + + // update the counter + powBlockCount++; + } + + var powActualTimeSpanIncPos = lastPowBlock.Header.Time - powBlockIterator.Header.Time; + + Assert(powBlockCount == amountOfPoWBlocks); + Assert(powIntervalsIncPosSum == powActualTimeSpanIncPos); + + var firstPowBlockHeaderTimeExceptGaps = lastPowBlock.Header.Time - powActualTimeSpanIncPos + posGaps; + return CalculatePoWTarget(lastPowBlock, firstPowBlockHeaderTimeExceptGaps, consensus); + } + + private Target GetNextPosTargetRequired(ChainedHeader lastPosBlock, IConsensus consensus) + { + // We'll need to go back 2 blocks, to calculate the time delta. We can only do that if the ratchet + // was active 2 blocks before the current lastPosBlock. So if that's not possible, we simply return + // the previous Target. Due to the precondition checks, we know it's a valid PoS Target. + if (!IsPosPowRatchetActiveAtHeight(lastPosBlock.Height - 2) || consensus.PosNoRetargeting) + { + return lastPosBlock.Header.Bits; + } + + // we are sure this is a PoW block because of the precondition checks, which previous blocks have already passed + ChainedHeader intermediatePowBlock = lastPosBlock.Previous; + + // we are sure this is a PoS block because of the precondition checks, which previous blocks have already passed + ChainedHeader prevLastPosBlock = intermediatePowBlock.Previous; + + // the time in seconds the intermediate PoS block has used, which is must be hidden for the calculation + var powGapSeconds = intermediatePowBlock.Header.Time - prevLastPosBlock.Header.Time; + + // add the powGapSeconds to the timestamp of the prevLastPosBlock, to compensate the time it took to create the PoW block + var adjustedPrevLastPowPosBlockTime = prevLastPosBlock.Header.Time + powGapSeconds; + + // pass in adjustedPrevLastPowPosBlockTime instead of the timestamp of the second block, and continue as normal + return CalculatePosRetarget(lastPosBlock.Header.Time, lastPosBlock.Header.Bits, adjustedPrevLastPowPosBlockTime, consensus.ProofOfStakeLimitV2, lastPosBlock.Height); + } + + private Target CalculatePosRetarget(uint lastBlockTime, Target lastBlockTarget, uint previousBlockTime, BigInteger targetLimit, int height) + { + uint targetSpacing = (uint)GetTargetSpacingTotalSeconds(height); // = 256s or 10 minutes after hf + uint actualSpacing = lastBlockTime - previousBlockTime; // this is never 0 or negative because that's a consensus rule + + // Limit the adjustment step by capping input values that are far from the average. + if (actualSpacing > targetSpacing * 4) // if the spacing was > 1024 seconds, pretend is was 1024 seconds + actualSpacing = targetSpacing * 4; + if (actualSpacing < targetSpacing / 4) // if the spacing was < 64 seconds, pretend is was 64 seconds + actualSpacing = targetSpacing / 4; + + BigInteger nextTarget = lastBlockTarget.ToBigInteger(); + + // To reduce the impact of randomness, the actualSpacing's weight is reduced to 1/32th (instead of 1/2). This creates + // similar results like using 32-period average. + // The problem with random spacing values is that they frequently lead to difficult adjustments in the wrong direction, + // when the sample size is as low as 1. + // The results with 1/2 were: PoS block ETA seconds: Average: 351, Median: 165. But average and median should have been 256 seconds. + long numerator = 31 * targetSpacing + actualSpacing; + long denominator = 32 * targetSpacing; + nextTarget = nextTarget.Multiply(BigInteger.ValueOf(numerator)); + nextTarget = nextTarget.Divide(BigInteger.ValueOf(denominator)); + + if (nextTarget.CompareTo(BigInteger.Zero) <= 0 || nextTarget.CompareTo(targetLimit) >= 1) + nextTarget = targetLimit; + + return new Target(nextTarget); + } + + private Target CalculatePoWTarget(ChainedHeader lastPowBlock, uint nFirstBlockTime, IConsensus consensus) + { + // This is used in tests to allow quickly mining blocks. + if (consensus.PowNoRetargeting) + { + return lastPowBlock.Header.Bits; + } + + int height = lastPowBlock.Height; + + // Limit adjustment step + long nActualTimespan = lastPowBlock.Header.Time - nFirstBlockTime; + if (nActualTimespan < GetTargetTimespanTotalSeconds(height) / 4) + nActualTimespan = (uint)GetTargetTimespanTotalSeconds(height) / 4; + if (nActualTimespan > GetTargetTimespanTotalSeconds(height) * 4) + nActualTimespan = (uint)GetTargetTimespanTotalSeconds(height) * 4; + + // Retarget + var bnNew = lastPowBlock.Header.Bits.ToBigInteger(); + bnNew = bnNew.Multiply(BigInteger.ValueOf(nActualTimespan)); + bnNew = bnNew.Divide(BigInteger.ValueOf((long)GetTargetTimespanTotalSeconds(height))); + + var finalTarget = new Target(bnNew); + if (finalTarget > consensus.PowLimit) + finalTarget = consensus.PowLimit; + + return finalTarget; + } + + private int GetPosPowRatchetIsActiveHeight() + { + switch (this.currentNetwork.NetworkType) + { + case NetworkType.Mainnet: + return PosPowRatchetIsActiveHeightMainNet; + case NetworkType.Testnet: + return PosPowRatchetIsActiveHeightTestNet; + case NetworkType.Regtest: + return PosPowRatchetIsActiveHeightInvalid; + } + + return PosPowRatchetIsActiveHeightInvalid; + } + + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local + private void Assert(bool condition, [CallerMemberName] string caller = "", [CallerLineNumber] int line = -1) + { + if (!condition) + { + throw new Exception($"{nameof(X1ConsensusOptions)} - assert failed! Caller {caller}, line: {line}."); + } + } + } +} \ No newline at end of file diff --git a/src/Networks/Blockcore.Networks.X1/Consensus/X1ProvenBlockHeader.cs b/src/Networks/Blockcore.Networks.X1/Consensus/X1ProvenBlockHeader.cs new file mode 100644 index 000000000..1d9934bdb --- /dev/null +++ b/src/Networks/Blockcore.Networks.X1/Consensus/X1ProvenBlockHeader.cs @@ -0,0 +1,15 @@ +using Blockcore.Consensus.BlockInfo; + +namespace Blockcore.Networks.X1.Consensus +{ + public class X1ProvenBlockHeader : ProvenBlockHeader + { + public X1ProvenBlockHeader() + { + } + + public X1ProvenBlockHeader(PosBlock block, X1BlockHeader x1BlockHeader) : base(block, x1BlockHeader) + { + } + } +} \ No newline at end of file diff --git a/src/Networks/Blockcore.Networks.X1/Consensus/X1Transaction.cs b/src/Networks/Blockcore.Networks.X1/Consensus/X1Transaction.cs new file mode 100644 index 000000000..c894df54d --- /dev/null +++ b/src/Networks/Blockcore.Networks.X1/Consensus/X1Transaction.cs @@ -0,0 +1,12 @@ +using Blockcore.Consensus.TransactionInfo; + +namespace Blockcore.Networks.X1.Consensus +{ + public class X1Transaction : Transaction + { + public override bool IsProtocolTransaction() + { + return this.IsCoinBase || this.IsCoinStake; + } + } +} \ No newline at end of file diff --git a/src/Networks/Blockcore.Networks.X1/Deployments/X1BIP9Deployments.cs b/src/Networks/Blockcore.Networks.X1/Deployments/X1BIP9Deployments.cs new file mode 100644 index 000000000..cf5b71394 --- /dev/null +++ b/src/Networks/Blockcore.Networks.X1/Deployments/X1BIP9Deployments.cs @@ -0,0 +1,56 @@ +using Blockcore.Base.Deployments; +using Blockcore.Consensus.ScriptInfo; +using Blockcore.Consensus.TransactionInfo; + +namespace Blockcore.Networks.X1.Deployments +{ + public class X1BIP9Deployments : BIP9DeploymentsArray + { + // The position of each deployment in the deployments array. + public const int TestDummy = 0; + + public const int ColdStaking = 1; + public const int CSV = 2; + public const int Segwit = 3; + + // The number of deployments. + public const int NumberOfDeployments = 4; + + /// + /// Constructs the BIP9 deployments array. + /// + public X1BIP9Deployments() : base(NumberOfDeployments) + { + } + + /// + /// Gets the deployment flags to set when the deployment activates. + /// + /// The deployment number. + /// The deployment flags. + public override BIP9DeploymentFlags GetFlags(int deployment) + { + BIP9DeploymentFlags flags = new BIP9DeploymentFlags(); + + switch (deployment) + { + case ColdStaking: + flags.ScriptFlags |= ScriptVerify.CheckColdStakeVerify; + break; + + case CSV: + // Start enforcing BIP68 (sequence locks), BIP112 (CHECKSEQUENCEVERIFY) and BIP113 (Median Time Past) using versionbits logic. + flags.ScriptFlags = ScriptVerify.CheckSequenceVerify; + flags.LockTimeFlags = Transaction.LockTimeFlags.VerifySequence | Transaction.LockTimeFlags.MedianTimePast; + break; + + case Segwit: + // Start enforcing WITNESS rules using versionbits logic. + flags.ScriptFlags = ScriptVerify.Witness; + break; + } + + return flags; + } + } +} \ No newline at end of file diff --git a/src/Networks/Blockcore.Networks.X1/Networks.cs b/src/Networks/Blockcore.Networks.X1/Networks.cs new file mode 100644 index 000000000..104191314 --- /dev/null +++ b/src/Networks/Blockcore.Networks.X1/Networks.cs @@ -0,0 +1,13 @@ +namespace Blockcore.Networks.X1 +{ + public static class Networks + { + public static NetworksSelector X1 + { + get + { + return new NetworksSelector(() => new X1Main(), () => new X1Test(), () => new X1RegTest()); + } + } + } +} \ No newline at end of file diff --git a/src/Networks/Blockcore.Networks.X1/Policies/X1StandardScriptsRegistry.cs b/src/Networks/Blockcore.Networks.X1/Policies/X1StandardScriptsRegistry.cs new file mode 100644 index 000000000..bdb9540f9 --- /dev/null +++ b/src/Networks/Blockcore.Networks.X1/Policies/X1StandardScriptsRegistry.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Linq; +using Blockcore.Consensus.ScriptInfo; +using Blockcore.Consensus.TransactionInfo; +using NBitcoin.BitcoinCore; + +namespace Blockcore.Networks.X1.Policies +{ + /// + /// X1-specific standard transaction definitions. + /// + public class X1StandardScriptsRegistry : StandardScriptsRegistry + { + // See MAX_OP_RETURN_RELAY in Bitcoin Core,