diff --git a/src/Stratis.Bitcoin.Features.Consensus/ConsensusController.cs b/src/Stratis.Bitcoin.Features.Consensus/ConsensusController.cs index 22ddb17ed9..91262e43c9 100644 --- a/src/Stratis.Bitcoin.Features.Consensus/ConsensusController.cs +++ b/src/Stratis.Bitcoin.Features.Consensus/ConsensusController.cs @@ -109,7 +109,13 @@ public IActionResult LockedInDeployments() List metrics = ruleEngine.NodeDeployments.BIP9.GetThresholdStateMetrics(this.ChainState.ConsensusTip.Previous, thresholdStates, activationHeights); - return this.Json(metrics.Select(m => new ThresholdActivationModel() { activationHeight = m.SinceHeight, DeploymentIndex = m.DeploymentIndex, DeploymentName = m.DeploymentName, Votes = m.Votes }).ToArray()); + return this.Json(metrics.Select(m => new ThresholdActivationModel() { + ActivationHeight = m.SinceHeight, + DeploymentIndex = m.DeploymentIndex, + DeploymentName = m.DeploymentName, + Votes = m.Votes, + LockedInHeight = m.SinceHeight - ruleEngine.Network.Consensus.MinerConfirmationWindow, + LockedInTimestamp = this.ChainIndexer[m.SinceHeight - ruleEngine.Network.Consensus.MinerConfirmationWindow].Header.Time}).ToArray()); } catch (Exception e) { diff --git a/src/Stratis.Bitcoin/Base/Deployments/Models/ThresholdActivationModel.cs b/src/Stratis.Bitcoin/Base/Deployments/Models/ThresholdActivationModel.cs index f36cb13707..21426e7b19 100644 --- a/src/Stratis.Bitcoin/Base/Deployments/Models/ThresholdActivationModel.cs +++ b/src/Stratis.Bitcoin/Base/Deployments/Models/ThresholdActivationModel.cs @@ -23,12 +23,24 @@ public class ThresholdActivationModel /// Activation height. /// [JsonProperty(PropertyName = "activationHeight")] - public int activationHeight { get; set; } + public int ActivationHeight { get; set; } /// /// Number of blocks with flags set that led to the deployment being locked in. /// [JsonProperty(PropertyName = "votes")] public int Votes { get; set; } + + /// + /// The height at which the deployment was locked-in. + /// + [JsonProperty(PropertyName = "lockedInHeight")] + public int? LockedInHeight { get; set; } + + /// + /// The timestamp of the blocked at the "lockedInHeight". + /// + [JsonProperty(PropertyName = "lockedInTimestamp")] + public long? LockedInTimestamp { get; set; } } } \ No newline at end of file diff --git a/src/Stratis.Bitcoin/Base/Deployments/ThresholdConditionCache.cs b/src/Stratis.Bitcoin/Base/Deployments/ThresholdConditionCache.cs index 2d6a32ec37..ea57273173 100644 --- a/src/Stratis.Bitcoin/Base/Deployments/ThresholdConditionCache.cs +++ b/src/Stratis.Bitcoin/Base/Deployments/ThresholdConditionCache.cs @@ -103,6 +103,8 @@ public List GetThresholdStateMetrics(ChainedHeader indexPre // Choose the last header that's within the window where voting led to locked-in state. sinceHeight = activationHeights[deploymentIndex]; indexPrev = referenceHeader.GetAncestor(sinceHeight - period - 1); + if (indexPrev == null) + continue; } else { diff --git a/src/Stratis.Features.FederatedPeg.Tests/ControllersTests/FederationGatewayControllerTests.cs b/src/Stratis.Features.FederatedPeg.Tests/ControllersTests/FederationGatewayControllerTests.cs index e016863eec..744ab85356 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/ControllersTests/FederationGatewayControllerTests.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/ControllersTests/FederationGatewayControllerTests.cs @@ -74,7 +74,7 @@ public FederationGatewayControllerTests() private FederationGatewayController CreateController(IFederatedPegSettings federatedPegSettings) { - var retrievalTypeConfirmations = new RetrievalTypeConfirmations(this.network, new NodeDeployments(this.network, new ChainIndexer(this.network)), federatedPegSettings); + var retrievalTypeConfirmations = new RetrievalTypeConfirmations(this.network, new NodeDeployments(this.network, new ChainIndexer(this.network)), federatedPegSettings, null, null); var controller = new FederationGatewayController( Substitute.For(), @@ -210,7 +210,7 @@ public void Call_Sidechain_Gateway_Get_Info() var federatedPegSettings = new FederatedPegSettings(nodeSettings, new CounterChainNetworkWrapper(KnownNetworks.StraxRegTest)); - var retrievalTypeConfirmations = new RetrievalTypeConfirmations(this.network, new NodeDeployments(this.network, new ChainIndexer(this.network)), federatedPegSettings); + var retrievalTypeConfirmations = new RetrievalTypeConfirmations(this.network, new NodeDeployments(this.network, new ChainIndexer(this.network)), federatedPegSettings, null, null); var controller = new FederationGatewayController( Substitute.For(), @@ -304,7 +304,7 @@ public void Call_Mainchain_Gateway_Get_Info() this.federationWalletManager.IsFederationWalletActive().Returns(true); var settings = new FederatedPegSettings(nodeSettings, new CounterChainNetworkWrapper(KnownNetworks.StraxRegTest)); - var retrievalTypeConfirmations = new RetrievalTypeConfirmations(this.network, new NodeDeployments(this.network, new ChainIndexer(this.network)), settings); + var retrievalTypeConfirmations = new RetrievalTypeConfirmations(this.network, new NodeDeployments(this.network, new ChainIndexer(this.network)), settings, null, null); var controller = new FederationGatewayController( Substitute.For(), diff --git a/src/Stratis.Features.FederatedPeg.Tests/DepositExtractorTests.cs b/src/Stratis.Features.FederatedPeg.Tests/DepositExtractorTests.cs index 72f607382b..81d8a79436 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/DepositExtractorTests.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/DepositExtractorTests.cs @@ -55,7 +55,7 @@ public DepositExtractorTests() this.depositExtractor = new DepositExtractor(this.conversionRequestRepository, this.federationSettings, this.network, this.opReturnDataReader); this.transactionBuilder = new TestTransactionBuilder(); - this.retrievalTypeConfirmations = new RetrievalTypeConfirmations(this.network, new NodeDeployments(this.network, new ChainIndexer(this.network)), this.federationSettings); + this.retrievalTypeConfirmations = new RetrievalTypeConfirmations(this.network, new NodeDeployments(this.network, new ChainIndexer(this.network)), this.federationSettings, null, null); } // Normal Deposits diff --git a/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksProviderTests.cs b/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksProviderTests.cs index b71fbf6c8d..dca776969e 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksProviderTests.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksProviderTests.cs @@ -80,7 +80,7 @@ public MaturedBlocksProviderTests() return this.blocks.SkipWhile(x => x.ChainedHeader.Height <= chainedHeader.Height).Where(x => x.ChainedHeader.Height <= this.consensusManager.Tip.Height).ToArray(); }); - this.retrievalTypeConfirmations = new RetrievalTypeConfirmations(this.network, new NodeDeployments(this.network, new ChainIndexer(this.network)), this.federatedPegSettings); + this.retrievalTypeConfirmations = new RetrievalTypeConfirmations(this.network, new NodeDeployments(this.network, new ChainIndexer(this.network)), this.federatedPegSettings, null, null); } [Fact] diff --git a/src/Stratis.Features.FederatedPeg/Controllers/CounterChainConsensusClient.cs b/src/Stratis.Features.FederatedPeg/Controllers/CounterChainConsensusClient.cs new file mode 100644 index 0000000000..cff0bc1f2f --- /dev/null +++ b/src/Stratis.Features.FederatedPeg/Controllers/CounterChainConsensusClient.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Stratis.Bitcoin.Base.Deployments.Models; +using Stratis.Bitcoin.Controllers; +using Stratis.Features.PoA.Collateral.CounterChain; + +namespace Stratis.Features.FederatedPeg.Controllers +{ + public class CounterChainConsensusClient : RestApiClientBase + { + /// + /// Accesses the consensus controller on the counter-chain. + /// + /// In a production/live scenario the sidechain and mainnet federation nodes should run on the same machine. + /// + /// + /// The counter-chain settings. + /// The implementation. + public CounterChainConsensusClient(ICounterChainSettings counterChainSettings, IHttpClientFactory httpClientFactory) + : base(httpClientFactory, counterChainSettings.CounterChainApiPort, "Consensus", $"http://{counterChainSettings.CounterChainApiHost}") + { + } + + public Task> GetLockedInDeployments(CancellationToken cancellation = default) + { + return this.SendGetRequestAsync>("lockedindeployments", null, cancellation); + } + } +} diff --git a/src/Stratis.Features.FederatedPeg/SourceChain/RetrievalTypeConfirmations.cs b/src/Stratis.Features.FederatedPeg/SourceChain/RetrievalTypeConfirmations.cs index be84381cfe..6bc4e89772 100644 --- a/src/Stratis.Features.FederatedPeg/SourceChain/RetrievalTypeConfirmations.cs +++ b/src/Stratis.Features.FederatedPeg/SourceChain/RetrievalTypeConfirmations.cs @@ -3,6 +3,8 @@ using NBitcoin; using Stratis.Bitcoin.Base.Deployments; using Stratis.Features.FederatedPeg.Interfaces; +using Stratis.Features.FederatedPeg.TargetChain; +using Stratis.Features.PoA.Collateral.CounterChain; namespace Stratis.Features.FederatedPeg.SourceChain { @@ -28,18 +30,24 @@ public class RetrievalTypeConfirmations : IRetrievalTypeConfirmations private const int MediumConfirmations = 25; // 1125 seconds - 18m45s private const int HighConfirmations = 50; // 2250 seconds - 37m30s private const int CirrusLowConfirmations = 30; // 480 seconds - 8m0s - private const int CirrusMediumConfirmation = 70; // 1120 seconds - 18m40s + private const int CirrusMediumConfirmations = 70; // 1120 seconds - 18m40s private const int CirrusHighConfirmations = 140; // 2240 seconds - 37m20s private readonly NodeDeployments nodeDeployments; private readonly Dictionary legacyRetrievalTypeConfirmations; private readonly Dictionary retrievalTypeConfirmations; private readonly Network network; + private readonly IMaturedBlocksSyncManager maturedBlocksSyncManager; + private readonly ICounterChainSettings counterChainSettings; + private readonly IFederatedPegSettings federatedPegSettings; - public RetrievalTypeConfirmations(Network network, NodeDeployments nodeDeployments, IFederatedPegSettings federatedPegSettings) + public RetrievalTypeConfirmations(Network network, NodeDeployments nodeDeployments, IFederatedPegSettings federatedPegSettings, IMaturedBlocksSyncManager maturedBlocksSyncManager, ICounterChainSettings counterChainSettings) { this.nodeDeployments = nodeDeployments; this.network = network; + this.maturedBlocksSyncManager = maturedBlocksSyncManager; + this.counterChainSettings = counterChainSettings; + this.federatedPegSettings = federatedPegSettings; this.legacyRetrievalTypeConfirmations = new Dictionary { [DepositRetrievalType.Small] = federatedPegSettings.MinimumConfirmationsSmallDeposits, @@ -47,50 +55,28 @@ public RetrievalTypeConfirmations(Network network, NodeDeployments nodeDeploymen [DepositRetrievalType.Large] = federatedPegSettings.MinimumConfirmationsLargeDeposits }; + this.retrievalTypeConfirmations = new Dictionary() + { + [DepositRetrievalType.Small] = federatedPegSettings.IsMainChain ? LowConfirmations : CirrusLowConfirmations, + [DepositRetrievalType.Normal] = federatedPegSettings.IsMainChain ? MediumConfirmations : CirrusMediumConfirmations, + [DepositRetrievalType.Large] = federatedPegSettings.IsMainChain ? HighConfirmations : CirrusHighConfirmations + }; + if (federatedPegSettings.IsMainChain) { this.legacyRetrievalTypeConfirmations[DepositRetrievalType.Distribution] = federatedPegSettings.MinimumConfirmationsDistributionDeposits; this.legacyRetrievalTypeConfirmations[DepositRetrievalType.ConversionSmall] = federatedPegSettings.MinimumConfirmationsSmallDeposits; this.legacyRetrievalTypeConfirmations[DepositRetrievalType.ConversionNormal] = federatedPegSettings.MinimumConfirmationsNormalDeposits; this.legacyRetrievalTypeConfirmations[DepositRetrievalType.ConversionLarge] = federatedPegSettings.MinimumConfirmationsLargeDeposits; - } - if (this.network.Name.StartsWith("Cirrus")) - { - this.retrievalTypeConfirmations = new Dictionary - { - [DepositRetrievalType.Small] = CirrusLowConfirmations, - [DepositRetrievalType.Normal] = CirrusMediumConfirmation, - [DepositRetrievalType.Large] = CirrusHighConfirmations - }; - - if (federatedPegSettings.IsMainChain) - { - this.retrievalTypeConfirmations[DepositRetrievalType.Distribution] = CirrusHighConfirmations; - this.retrievalTypeConfirmations[DepositRetrievalType.ConversionSmall] = CirrusLowConfirmations; - this.retrievalTypeConfirmations[DepositRetrievalType.ConversionNormal] = CirrusMediumConfirmation; - this.retrievalTypeConfirmations[DepositRetrievalType.ConversionLarge] = CirrusHighConfirmations; - } - } - else - { - this.retrievalTypeConfirmations = new Dictionary() - { - [DepositRetrievalType.Small] = LowConfirmations, - [DepositRetrievalType.Normal] = MediumConfirmations, - [DepositRetrievalType.Large] = HighConfirmations - }; - - if (federatedPegSettings.IsMainChain) - { - this.retrievalTypeConfirmations[DepositRetrievalType.Distribution] = HighConfirmations; - this.retrievalTypeConfirmations[DepositRetrievalType.ConversionSmall] = LowConfirmations; - this.retrievalTypeConfirmations[DepositRetrievalType.ConversionNormal] = MediumConfirmations; - this.retrievalTypeConfirmations[DepositRetrievalType.ConversionLarge] = HighConfirmations; - } + this.retrievalTypeConfirmations[DepositRetrievalType.Distribution] = HighConfirmations; + this.retrievalTypeConfirmations[DepositRetrievalType.ConversionSmall] = LowConfirmations; + this.retrievalTypeConfirmations[DepositRetrievalType.ConversionNormal] = MediumConfirmations; + this.retrievalTypeConfirmations[DepositRetrievalType.ConversionLarge] = HighConfirmations; } } + /// public int MaximumConfirmationsAtMaturityHeight(int maturityHeight) { if (maturityHeight < this.Release1300ActivationHeight) @@ -99,8 +85,7 @@ public int MaximumConfirmationsAtMaturityHeight(int maturityHeight) return this.retrievalTypeConfirmations.Values.Max(); } - private int Release1300ActivationHeight => (this.nodeDeployments?.BIP9.ArraySize > 0) ? this.nodeDeployments.BIP9.ActivationHeightProviders[0 /* Release 1300 */].ActivationHeight : 0; - + /// public int GetDepositConfirmations(int depositHeight, DepositRetrievalType retrievalType) { // Keep everything maturity-height-centric. Otherwise the way we use MaximumConfirmationsAtMaturityHeight will have to change as well. @@ -110,14 +95,31 @@ public int GetDepositConfirmations(int depositHeight, DepositRetrievalType retri return this.retrievalTypeConfirmations[retrievalType]; } + /// public int GetDepositMaturityHeight(int depositHeight, DepositRetrievalType retrievalType) { return depositHeight + GetDepositConfirmations(depositHeight, retrievalType); } + /// public DepositRetrievalType[] GetRetrievalTypes() { return this.retrievalTypeConfirmations.Keys.ToArray(); } + + private int Release1300ActivationHeight + { + get + { + if (!this.federatedPegSettings.IsMainChain) + return (this.nodeDeployments?.BIP9.ArraySize > 0) ? this.nodeDeployments.BIP9.ActivationHeightProviders[0 /* Release 1300 */].ActivationHeight : 0; + + // This code is running on the main chain. + if (this.counterChainSettings.CounterChainNetwork.Consensus.BIP9Deployments.Length == 0) + return 0; + + return this.maturedBlocksSyncManager.GetMainChainActivationHeight(); + } + } } } diff --git a/src/Stratis.Features.FederatedPeg/TargetChain/MaturedBlocksSyncManager.cs b/src/Stratis.Features.FederatedPeg/TargetChain/MaturedBlocksSyncManager.cs index 96811693d8..f15e26ecbd 100644 --- a/src/Stratis.Features.FederatedPeg/TargetChain/MaturedBlocksSyncManager.cs +++ b/src/Stratis.Features.FederatedPeg/TargetChain/MaturedBlocksSyncManager.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using NBitcoin; using Stratis.Bitcoin.AsyncWork; +using Stratis.Bitcoin.Base.Deployments.Models; using Stratis.Bitcoin.Configuration.Logging; using Stratis.Bitcoin.Features.ExternalApi; using Stratis.Bitcoin.Features.PoA; @@ -17,6 +19,7 @@ using Stratis.Features.FederatedPeg.Models; using Stratis.Features.FederatedPeg.SourceChain; using Stratis.Features.FederatedPeg.Wallet; +using Stratis.Features.PoA.Collateral.CounterChain; namespace Stratis.Features.FederatedPeg.TargetChain { @@ -38,11 +41,14 @@ public interface IMaturedBlocksSyncManager : IDisposable /// The deposit that will be injected into the that distributes a fee to all the multisig nodes /// for submitting a interop transfer. This is currently used for SRC20 to ERC20 transfers. void AddInterOpFeeDeposit(IDeposit deposit); + + int GetMainChainActivationHeight(); } /// public class MaturedBlocksSyncManager : IMaturedBlocksSyncManager { + private const string Release1300DeploymentNameLower = "release1300"; private readonly IAsyncProvider asyncProvider; private readonly ICrossChainTransferStore crossChainTransferStore; private readonly IFederationGatewayClient federationGatewayClient; @@ -53,11 +59,14 @@ public class MaturedBlocksSyncManager : IMaturedBlocksSyncManager private readonly ILogger logger; private readonly INodeLifetime nodeLifetime; private readonly IConversionRequestRepository conversionRequestRepository; + private readonly ICounterChainSettings counterChainSettings; + private readonly IHttpClientFactory httpClientFactory; private readonly ChainIndexer chainIndexer; private readonly IExternalApiPoller externalApiPoller; private readonly IConversionRequestFeeService conversionRequestFeeService; private readonly Network network; private readonly IFederationManager federationManager; + private int mainChainActivationHeight; private IAsyncLoop requestDepositsTask; /// When we are fully synced we stop asking for more blocks for this amount of time. @@ -85,7 +94,9 @@ public MaturedBlocksSyncManager( IFederatedPegSettings federatedPegSettings, IFederationManager federationManager = null, IExternalApiPoller externalApiPoller = null, - IConversionRequestFeeService conversionRequestFeeService = null) + IConversionRequestFeeService conversionRequestFeeService = null, + ICounterChainSettings counterChainSettings = null, + IHttpClientFactory httpClientFactory = null) { this.asyncProvider = asyncProvider; this.chainIndexer = chainIndexer; @@ -97,24 +108,75 @@ public MaturedBlocksSyncManager( this.initialBlockDownloadState = initialBlockDownloadState; this.nodeLifetime = nodeLifetime; this.conversionRequestRepository = conversionRequestRepository; + this.counterChainSettings = counterChainSettings; + this.httpClientFactory = httpClientFactory; this.chainIndexer = chainIndexer; this.externalApiPoller = externalApiPoller; this.conversionRequestFeeService = conversionRequestFeeService; this.network = network; this.federationManager = federationManager; + this.mainChainActivationHeight = int.MaxValue; this.lockObject = new object(); this.logger = LogManager.GetCurrentClassLogger(); } + public void RecordCounterChainActivations() + { + // If this is the main chain then ask the side-chain for its activation height. + if (!this.federatedPegSettings.IsMainChain) + return; + + // Ensures that we only check this once on startup. + if (this.mainChainActivationHeight != int.MaxValue) + return; + + CounterChainConsensusClient consensusClient = new CounterChainConsensusClient(this.counterChainSettings, this.httpClientFactory); + List lockedInActivations = consensusClient.GetLockedInDeployments(this.nodeLifetime.ApplicationStopping).ConfigureAwait(false).GetAwaiter().GetResult(); + if (lockedInActivations == null || lockedInActivations.Count == 0) + { + this.logger.LogDebug("There are {0} locked-in deployments.", lockedInActivations?.Count); + return; + } + + ThresholdActivationModel model = lockedInActivations.FirstOrDefault(a => a.DeploymentName.ToLowerInvariant() == Release1300DeploymentNameLower); + if (model == null || model.LockedInTimestamp == null) + { + this.logger.LogDebug("There are no locked-in deployments for '{0}'.", Release1300DeploymentNameLower); + return; + } + + if (this.chainIndexer.Tip.Header.Time < model.LockedInTimestamp.Value) + { + this.logger.LogDebug("The chain tip time {0} is still below the locked in time {1}.", this.chainIndexer.Tip.Header.Time, model.LockedInTimestamp.Value); + return; + } + + // The above condition ensures that the 'Last' below will always return a value. + int mainChainLockedInHeight = this.chainIndexer.Tip.EnumerateToGenesis().TakeWhile(h => h.Header.Time >= (uint)(model.LockedInTimestamp)).Last().Height; + + Network counterChainNetwork = this.counterChainSettings.CounterChainNetwork; + this.mainChainActivationHeight = mainChainLockedInHeight + + (int)((counterChainNetwork.Consensus.MinerConfirmationWindow * counterChainNetwork.Consensus.TargetSpacing.TotalSeconds) / this.network.Consensus.TargetSpacing.TotalSeconds); + } + + public int GetMainChainActivationHeight() + { + return this.mainChainActivationHeight; + } + /// public async Task StartAsync() { // Initialization delay; give the counter chain node some time to start it's API service. await Task.Delay(TimeSpan.FromSeconds(InitializationDelaySeconds), this.nodeLifetime.ApplicationStopping).ConfigureAwait(false); + RecordCounterChainActivations(); + this.requestDepositsTask = this.asyncProvider.CreateAndRunAsyncLoop($"{nameof(MaturedBlocksSyncManager)}.{nameof(this.requestDepositsTask)}", async token => { + RecordCounterChainActivations(); + bool delayRequired = await this.SyncDepositsAsync().ConfigureAwait(false); if (delayRequired) {