diff --git a/src/NBitcoin.Tests/sigopcount_tests.cs b/src/NBitcoin.Tests/sigopcount_tests.cs index 28d7c1a754..347f039933 100644 --- a/src/NBitcoin.Tests/sigopcount_tests.cs +++ b/src/NBitcoin.Tests/sigopcount_tests.cs @@ -1,4 +1,5 @@ using System.Linq; +using Moq; using Stratis.Bitcoin.Tests.Common; using Xunit; @@ -39,5 +40,24 @@ public void GetSigOpCount() scriptSig2 = scriptSig2 + OpcodeType.OP_1 + dummy.ToBytes() + dummy.ToBytes() + s2.ToBytes(); Assert.Equal(3U, p2sh.GetSigOpCount(KnownNetworks.Main, scriptSig2)); } + + [Fact] + [Trait("Core", "Core")] + public void GetSigOpCountForFederation() + { + PubKey[] keys = Enumerable.Range(0, 3).Select(_ => new Key(true).PubKey).ToArray(); + var federations = new Federations(); + federations.RegisterFederation(new Federation(keys.Take(2), 1)); + var network = KnownNetworks.StraxRegTest; + network.SetPrivatePropertyValue("Federations", federations); + + // Test CScript::GetSigOpCount() + var s1 = new Script(); + s1 = s1 + OpcodeType.OP_1 + OpcodeType.OP_FEDERATION + OpcodeType.OP_CHECKMULTISIG; + Assert.Equal(2U, s1.GetSigOpCount(true, network)); + s1 = s1 + OpcodeType.OP_IF + OpcodeType.OP_CHECKSIG + OpcodeType.OP_ENDIF; + Assert.Equal(3U, s1.GetSigOpCount(true, network)); + Assert.Equal(21U, s1.GetSigOpCount(false, network)); + } } } diff --git a/src/NBitcoin/Network.cs b/src/NBitcoin/Network.cs index 1825f854da..eb69a200d1 100644 --- a/src/NBitcoin/Network.cs +++ b/src/NBitcoin/Network.cs @@ -456,6 +456,11 @@ public byte[] MagicBytes /// public string CirrusRewardDummyAddress { get; protected set; } + /// + /// This is used for conversion transaction fee distribution transactions. + /// + public string ConversionTransactionFeeDistributionDummyAddress { get; protected set; } + /// /// The height at which reward batching will be activated. /// diff --git a/src/NBitcoin/Script.cs b/src/NBitcoin/Script.cs index e655abe7de..fa31eca5a2 100644 --- a/src/NBitcoin/Script.cs +++ b/src/NBitcoin/Script.cs @@ -854,7 +854,7 @@ public IList ToOps() } } - public uint GetSigOpCount(bool fAccurate) + public uint GetSigOpCount(bool fAccurate, Network network = null) { uint n = 0; Op lastOpcode = null; @@ -864,7 +864,9 @@ public uint GetSigOpCount(bool fAccurate) n++; else if (op.Code == OpcodeType.OP_CHECKMULTISIG || op.Code == OpcodeType.OP_CHECKMULTISIGVERIFY) { - if (fAccurate && lastOpcode != null && lastOpcode.Code >= OpcodeType.OP_1 && lastOpcode.Code <= OpcodeType.OP_16) + if (fAccurate && network?.Federations != null && lastOpcode.Code == OpcodeType.OP_FEDERATION) + n += (uint)network.Federations.GetOnlyFederation().GetFederationDetails().transactionSigningKeys.Length; + else if (fAccurate && lastOpcode != null && lastOpcode.Code >= OpcodeType.OP_1 && lastOpcode.Code <= OpcodeType.OP_16) n += (lastOpcode.PushData == null || lastOpcode.PushData.Length == 0) ? 0U : (uint)lastOpcode.PushData[0]; else n += 20; @@ -913,12 +915,12 @@ public uint GetSigOpCount(Network network, Script scriptSig) { // TODO: Is the network needed? if (!IsPayToScriptHash(network)) - return GetSigOpCount(true); + return GetSigOpCount(true, network); // This is a pay-to-script-hash scriptPubKey; // get the last item that the scriptSig // pushes onto the stack: bool validSig = new PayToScriptHashTemplate().CheckScriptSig(network, scriptSig, this); - return !validSig ? 0 : new Script(scriptSig.ToOps().Last().PushData).GetSigOpCount(true); + return !validSig ? 0 : new Script(scriptSig.ToOps().Last().PushData).GetSigOpCount(true, network); // ... and return its opcount: } diff --git a/src/Stratis.Bitcoin.Features.Api/NodeController.cs b/src/Stratis.Bitcoin.Features.Api/NodeController.cs index b9ed0bd069..934a2b4c5b 100644 --- a/src/Stratis.Bitcoin.Features.Api/NodeController.cs +++ b/src/Stratis.Bitcoin.Features.Api/NodeController.cs @@ -71,6 +71,8 @@ public class NodeController : Controller /// An interface implementation used to retrieve unspent transactions. private readonly IGetUnspentTransaction getUnspentTransaction; + private readonly IInitialBlockDownloadState initialBlockDownloadState; + /// Specification of the network the node runs on. private Network network; // Not readonly because of ValidateAddress @@ -97,6 +99,7 @@ public NodeController( ISelfEndpointTracker selfEndpointTracker, IConsensusManager consensusManager, IBlockStore blockStore, + IInitialBlockDownloadState initialBlockDownloadState, IGetUnspentTransaction getUnspentTransaction = null, INetworkDifficulty networkDifficulty = null, IPooledGetUnspentTransaction pooledGetUnspentTransaction = null, @@ -127,6 +130,7 @@ public NodeController( this.consensusManager = consensusManager; this.blockStore = blockStore; this.getUnspentTransaction = getUnspentTransaction; + this.initialBlockDownloadState = initialBlockDownloadState; this.networkDifficulty = networkDifficulty; this.pooledGetUnspentTransaction = pooledGetUnspentTransaction; this.pooledTransaction = pooledTransaction; @@ -158,7 +162,8 @@ public IActionResult Status() RunningTime = this.dateTimeProvider.GetUtcNow() - this.fullNode.StartTime, CoinTicker = this.network.CoinTicker, State = this.fullNode.State.ToString(), - BestPeerHeight = this.chainState.BestPeerTip?.Height + BestPeerHeight = this.chainState.BestPeerTip?.Height, + InIbd = this.initialBlockDownloadState.IsInitialBlockDownload() }; // Add the list of features that are enabled. diff --git a/src/Stratis.Bitcoin.Features.BlockStore/Models/AddressIndexerTipModel.cs b/src/Stratis.Bitcoin.Features.BlockStore/Models/AddressIndexerTipModel.cs index 9cc98a30b7..20f91805a2 100644 --- a/src/Stratis.Bitcoin.Features.BlockStore/Models/AddressIndexerTipModel.cs +++ b/src/Stratis.Bitcoin.Features.BlockStore/Models/AddressIndexerTipModel.cs @@ -1,10 +1,14 @@ using NBitcoin; +using Newtonsoft.Json; +using Stratis.Bitcoin.Utilities.JsonConverters; namespace Stratis.Bitcoin.Features.BlockStore.Models { public sealed class AddressIndexerTipModel { + [JsonConverter(typeof(UInt256JsonConverter))] public uint256 TipHash { get; set; } + public int? TipHeight { get; set; } } } \ No newline at end of file diff --git a/src/Stratis.Bitcoin.Features.Consensus.Tests/InitialBlockDownloadTest.cs b/src/Stratis.Bitcoin.Features.Consensus.Tests/InitialBlockDownloadTest.cs index f486a8bab3..d7897826a9 100644 --- a/src/Stratis.Bitcoin.Features.Consensus.Tests/InitialBlockDownloadTest.cs +++ b/src/Stratis.Bitcoin.Features.Consensus.Tests/InitialBlockDownloadTest.cs @@ -34,7 +34,7 @@ public InitialBlockDownloadTest() public void InIBDIfChainTipIsNull() { this.chainState.ConsensusTip = null; - var blockDownloadState = new InitialBlockDownloadState(this.chainState, this.network, this.consensusSettings, this.checkpoints, this.loggerFactory.Object, DateTimeProvider.Default); + var blockDownloadState = new InitialBlockDownloadState(this.chainState, this.network, this.consensusSettings, this.checkpoints, DateTimeProvider.Default); Assert.True(blockDownloadState.IsInitialBlockDownload()); } @@ -43,7 +43,7 @@ public void InIBDIfBehindCheckpoint() { BlockHeader blockHeader = this.network.Consensus.ConsensusFactory.CreateBlockHeader(); this.chainState.ConsensusTip = new ChainedHeader(blockHeader, blockHeader.GetHash(), 1000); - var blockDownloadState = new InitialBlockDownloadState(this.chainState, this.network, this.consensusSettings, this.checkpoints, this.loggerFactory.Object, DateTimeProvider.Default); + var blockDownloadState = new InitialBlockDownloadState(this.chainState, this.network, this.consensusSettings, this.checkpoints, DateTimeProvider.Default); Assert.True(blockDownloadState.IsInitialBlockDownload()); } @@ -52,7 +52,7 @@ public void InIBDIfChainWorkIsLessThanMinimum() { BlockHeader blockHeader = this.network.Consensus.ConsensusFactory.CreateBlockHeader(); this.chainState.ConsensusTip = new ChainedHeader(blockHeader, blockHeader.GetHash(), this.checkpoints.GetLastCheckpointHeight() + 1); - var blockDownloadState = new InitialBlockDownloadState(this.chainState, this.network, this.consensusSettings, this.checkpoints, this.loggerFactory.Object, DateTimeProvider.Default); + var blockDownloadState = new InitialBlockDownloadState(this.chainState, this.network, this.consensusSettings, this.checkpoints, DateTimeProvider.Default); Assert.True(blockDownloadState.IsInitialBlockDownload()); } @@ -68,7 +68,7 @@ public void InIBDIfTipIsOlderThanMaxAge() blockHeader.Time = ((uint) DateTimeOffset.Now.ToUnixTimeSeconds()) - (uint) this.network.MaxTipAge - 1; this.chainState.ConsensusTip = new ChainedHeader(blockHeader, blockHeader.GetHash(), this.checkpoints.GetLastCheckpointHeight() + 1); - var blockDownloadState = new InitialBlockDownloadState(this.chainState, this.network, this.consensusSettings, this.checkpoints, this.loggerFactory.Object, DateTimeProvider.Default); + var blockDownloadState = new InitialBlockDownloadState(this.chainState, this.network, this.consensusSettings, this.checkpoints, DateTimeProvider.Default); Assert.True(blockDownloadState.IsInitialBlockDownload()); } } diff --git a/src/Stratis.Bitcoin.Features.Consensus.Tests/Rules/CommonRules/PosCoinViewRuleTest.cs b/src/Stratis.Bitcoin.Features.Consensus.Tests/Rules/CommonRules/PosCoinViewRuleTest.cs index 8722b8fda4..4948d6612b 100644 --- a/src/Stratis.Bitcoin.Features.Consensus.Tests/Rules/CommonRules/PosCoinViewRuleTest.cs +++ b/src/Stratis.Bitcoin.Features.Consensus.Tests/Rules/CommonRules/PosCoinViewRuleTest.cs @@ -35,7 +35,7 @@ public class PosCoinViewRuleTests : TestPosConsensusRulesUnitTestBase private async Task CreateConsensusManagerAsync(Dictionary unspentOutputs) { this.consensusSettings = new ConsensusSettings(Configuration.NodeSettings.Default(this.network)); - var initialBlockDownloadState = new InitialBlockDownloadState(this.chainState.Object, this.network, this.consensusSettings, new Checkpoints(), this.loggerFactory.Object, DateTimeProvider.Default); + var initialBlockDownloadState = new InitialBlockDownloadState(this.chainState.Object, this.network, this.consensusSettings, new Checkpoints(), DateTimeProvider.Default); var signals = new Signals.Signals(this.loggerFactory.Object, null); var asyncProvider = new AsyncProvider(this.loggerFactory.Object, signals); diff --git a/src/Stratis.Bitcoin.Features.Consensus.Tests/Rules/CommonRules/StraxCoinViewRuleTests.cs b/src/Stratis.Bitcoin.Features.Consensus.Tests/Rules/CommonRules/StraxCoinViewRuleTests.cs index 240b284609..669d820221 100644 --- a/src/Stratis.Bitcoin.Features.Consensus.Tests/Rules/CommonRules/StraxCoinViewRuleTests.cs +++ b/src/Stratis.Bitcoin.Features.Consensus.Tests/Rules/CommonRules/StraxCoinViewRuleTests.cs @@ -35,7 +35,7 @@ public class StraxCoinViewRuleTests : TestPosConsensusRulesUnitTestBase private async Task CreateConsensusManagerAsync(Dictionary unspentOutputs) { this.consensusSettings = new ConsensusSettings(Configuration.NodeSettings.Default(this.network)); - var initialBlockDownloadState = new InitialBlockDownloadState(this.chainState.Object, this.network, this.consensusSettings, new Checkpoints(), this.loggerFactory.Object, DateTimeProvider.Default); + var initialBlockDownloadState = new InitialBlockDownloadState(this.chainState.Object, this.network, this.consensusSettings, new Checkpoints(), DateTimeProvider.Default); var signals = new Signals.Signals(this.loggerFactory.Object, null); var asyncProvider = new AsyncProvider(this.loggerFactory.Object, signals); diff --git a/src/Stratis.Bitcoin.Features.Consensus.Tests/TestChainFactory.cs b/src/Stratis.Bitcoin.Features.Consensus.Tests/TestChainFactory.cs index 62dfe27d35..59d4265931 100644 --- a/src/Stratis.Bitcoin.Features.Consensus.Tests/TestChainFactory.cs +++ b/src/Stratis.Bitcoin.Features.Consensus.Tests/TestChainFactory.cs @@ -116,7 +116,7 @@ public static async Task CreateAsync(Network network, string d testChainContext.Checkpoints = new Checkpoints(); testChainContext.ChainIndexer = new ChainIndexer(network); testChainContext.ChainState = new ChainState(); - testChainContext.InitialBlockDownloadState = new InitialBlockDownloadState(testChainContext.ChainState, testChainContext.Network, consensusSettings, new Checkpoints(), testChainContext.NodeSettings.LoggerFactory, testChainContext.DateTimeProvider); + testChainContext.InitialBlockDownloadState = new InitialBlockDownloadState(testChainContext.ChainState, testChainContext.Network, consensusSettings, new Checkpoints(), testChainContext.DateTimeProvider); var inMemoryCoinView = new InMemoryCoinView(new HashHeightPair(testChainContext.ChainIndexer.Tip)); var cachedCoinView = new CachedCoinView(network, new Checkpoints(), inMemoryCoinView, DateTimeProvider.Default, testChainContext.LoggerFactory, new NodeStats(testChainContext.DateTimeProvider, testChainContext.NodeSettings, new Mock().Object), new ConsensusSettings(testChainContext.NodeSettings)); diff --git a/src/Stratis.Bitcoin.Features.Consensus/Rules/CommonRules/CoinviewRule.cs b/src/Stratis.Bitcoin.Features.Consensus/Rules/CommonRules/CoinviewRule.cs index 75ff69c23a..9bbd88f2ba 100644 --- a/src/Stratis.Bitcoin.Features.Consensus/Rules/CommonRules/CoinviewRule.cs +++ b/src/Stratis.Bitcoin.Features.Consensus/Rules/CommonRules/CoinviewRule.cs @@ -365,7 +365,7 @@ private long CountWitnessSignatureOperation(Script scriptPubKey, WitScript witne if (witParams.Program.Length == 32 && witness.PushCount > 0) { Script subscript = Script.FromBytesUnsafe(witness.GetUnsafePush(witness.PushCount - 1)); - return subscript.GetSigOpCount(true); + return subscript.GetSigOpCount(true, this.Parent.Network); } } diff --git a/src/Stratis.Bitcoin.Features.ExternalAPI/ApiClients/CoinGeckoClient.cs b/src/Stratis.Bitcoin.Features.ExternalAPI/ApiClients/CoinGeckoClient.cs new file mode 100644 index 0000000000..9f1ff07c08 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.ExternalAPI/ApiClients/CoinGeckoClient.cs @@ -0,0 +1,63 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Stratis.Bitcoin.Features.ExternalApi.Models; + +namespace Stratis.Bitcoin.Features.ExternalApi.ApiClients +{ + public class CoinGeckoClient : IDisposable + { + public const string DummyUserAgent = "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"; + + private readonly ExternalApiSettings externalApiSettings; + private readonly HttpClient client; + + private decimal stratisPrice = -1; + private decimal ethereumPrice = -1; + + public CoinGeckoClient(ExternalApiSettings externalApiSettings) + { + this.externalApiSettings = externalApiSettings; + + this.client = new HttpClient(); + } + + public decimal GetStratisPrice() + { + return this.stratisPrice; + } + + public decimal GetEthereumPrice() + { + return this.ethereumPrice; + } + + public async Task PriceDataRetrievalAsync() + { + var targetUri = new Uri(this.externalApiSettings.PriceUrl); + var requestMessage = new HttpRequestMessage(HttpMethod.Get, targetUri); + requestMessage.Headers.TryAddWithoutValidation("User-Agent", DummyUserAgent); + + HttpResponseMessage resp = await this.client.SendAsync(requestMessage).ConfigureAwait(false); + string content = await resp.Content.ReadAsStringAsync().ConfigureAwait(false); + + CoinGeckoResponse response = JsonConvert.DeserializeObject(content); + + if (response?.stratis == null || response?.ethereum == null) + { + return null; + } + + this.stratisPrice = response.stratis.usd; + this.ethereumPrice = response.ethereum.usd; + + return response; + } + + public void Dispose() + { + this.client?.Dispose(); + } + } +} diff --git a/src/Stratis.Bitcoin.Features.ExternalAPI/ApiClients/EtherscanClient.cs b/src/Stratis.Bitcoin.Features.ExternalAPI/ApiClients/EtherscanClient.cs new file mode 100644 index 0000000000..dcf815b0dc --- /dev/null +++ b/src/Stratis.Bitcoin.Features.ExternalAPI/ApiClients/EtherscanClient.cs @@ -0,0 +1,110 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Stratis.Bitcoin.Features.ExternalApi.Models; + +namespace Stratis.Bitcoin.Features.ExternalApi.ApiClients +{ + public class EtherscanClient : IDisposable + { + private readonly ExternalApiSettings externalApiSettings; + private readonly HttpClient client; + + private int[] fastSamples = new[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + private int[] proposeSamples = new [] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + private int[] safeSamples = new[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + + private bool sampled = false; + private int samplePointer = 0; + + public EtherscanClient(ExternalApiSettings externalApiSettings) + { + this.externalApiSettings = externalApiSettings; + + this.client = new HttpClient(); + } + + /// + /// Retrieves a recommended gas price based on historical measured samples. + /// + /// We use the average of the historic proposed price. + /// The recommended gas price in gwei. Returns -1 if price data is not yet available. + public int GetGasPrice() + { + if (!this.sampled) + { + return -1; + } + + // In future this could be made more responsive to sudden changes in the recent price, and possibly use the safe price if it can be reasonably anticipated that the transaction will still confirm timeously. + + // The weightings should add up to 1. + decimal fastWeighting = 0.0m; + decimal proposedWeighting = 1.0m; + decimal safeWeighting = 0.0m; + + decimal totalFast = 0m; + decimal totalProposed = 0m; + decimal totalSafe = 0m; + + for (int i = 0; i < this.fastSamples.Length; i++) + { + totalFast += this.fastSamples[i]; + totalProposed += this.proposeSamples[i]; + totalSafe += this.safeSamples[i]; + } + + return (int)Math.Ceiling(((totalFast * fastWeighting) + (totalProposed * proposedWeighting) + (totalSafe * safeWeighting) / this.fastSamples.Length)); + } + + public async Task GasOracle(bool recordSamples) + { + string content = await this.client.GetStringAsync(this.externalApiSettings.EtherscanGasOracleUrl); + + EtherscanGasOracleResponse response = JsonConvert.DeserializeObject(content); + + if (response?.result == null) + { + return null; + } + + // We do not know how long the node was shut down for, so the very first sample must populate every array element (regardless of whether the caller requested sample recording). + // There would be little point storing the historic data in the key value store, for instance. + if (!this.sampled) + { + for (int i = 0; i < this.fastSamples.Length; i++) + { + this.fastSamples[i] = response.result.FastGasPrice; + this.proposeSamples[i] = response.result.ProposeGasPrice; + this.safeSamples[i] = response.result.SafeGasPrice; + } + + this.sampled = true; + + return response; + } + + if (recordSamples) + { + this.fastSamples[this.samplePointer] = response.result.FastGasPrice; + this.proposeSamples[this.samplePointer] = response.result.ProposeGasPrice; + this.safeSamples[this.samplePointer] = response.result.SafeGasPrice; + + this.samplePointer++; + + if (this.samplePointer > this.fastSamples.Length) + { + this.samplePointer = 0; + } + } + + return response; + } + + public void Dispose() + { + this.client?.Dispose(); + } + } +} diff --git a/src/Stratis.Bitcoin.Features.ExternalAPI/Controllers/ExternalApiController.cs b/src/Stratis.Bitcoin.Features.ExternalAPI/Controllers/ExternalApiController.cs new file mode 100644 index 0000000000..7758f6f0d1 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.ExternalAPI/Controllers/ExternalApiController.cs @@ -0,0 +1,114 @@ +using System; +using System.Net; +using Microsoft.AspNetCore.Mvc; +using NLog; +using Stratis.Bitcoin.Features.ExternalApi; +using Stratis.Bitcoin.Utilities.JsonErrors; + +namespace Stratis.Features.ExternalApi.Controllers +{ + [ApiVersion("1")] + [Route("api/[controller]")] + public class ExternalApiController : Controller + { + private readonly IExternalApiPoller externalApiPoller; + + private readonly ILogger logger; + + public ExternalApiController(IExternalApiPoller externalApiPoller) + { + this.externalApiPoller = externalApiPoller; + this.logger = LogManager.GetCurrentClassLogger(); + } + + [Route("estimateconversiongas")] + [HttpGet] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public IActionResult EstimateConversionGas() + { + try + { + return this.Json(this.externalApiPoller.EstimateConversionTransactionGas()); + } + catch (Exception e) + { + this.logger.Error("Exception occurred: {0}", e.ToString()); + return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString()); + } + } + + [Route("estimateconversionfee")] + [HttpGet] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public IActionResult EstimateConversionFee() + { + try + { + return this.Json(this.externalApiPoller.EstimateConversionTransactionFee()); + } + catch (Exception e) + { + this.logger.Error("Exception occurred: {0}", e.ToString()); + return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString()); + } + } + + [Route("gasprice")] + [HttpGet] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public IActionResult GasPrice() + { + try + { + return this.Json(this.externalApiPoller.GetGasPrice()); + } + catch (Exception e) + { + this.logger.Error("Exception occurred: {0}", e.ToString()); + return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString()); + } + } + + [Route("stratisprice")] + [HttpGet] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public IActionResult StratisPrice() + { + try + { + return this.Json(this.externalApiPoller.GetStratisPrice()); + } + catch (Exception e) + { + this.logger.Error("Exception occurred: {0}", e.ToString()); + return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString()); + } + } + + [Route("ethereumprice")] + [HttpGet] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public IActionResult EthereumPrice() + { + try + { + return this.Json(this.externalApiPoller.GetEthereumPrice()); + } + catch (Exception e) + { + this.logger.Error("Exception occurred: {0}", e.ToString()); + return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString()); + } + } + } +} diff --git a/src/Stratis.Bitcoin.Features.ExternalAPI/ExternalApiFeature.cs b/src/Stratis.Bitcoin.Features.ExternalAPI/ExternalApiFeature.cs new file mode 100644 index 0000000000..d976b50157 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.ExternalAPI/ExternalApiFeature.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Stratis.Bitcoin.Builder; +using Stratis.Bitcoin.Builder.Feature; +using Stratis.Bitcoin.Configuration.Logging; + +namespace Stratis.Bitcoin.Features.ExternalApi +{ + public sealed class ExternalApiFeature : FullNodeFeature + { + private readonly IExternalApiPoller externalApiPoller; + + public ExternalApiFeature(IExternalApiPoller externalApiPoller) + { + this.externalApiPoller = externalApiPoller; + } + + public override Task InitializeAsync() + { + this.externalApiPoller?.Initialize(); + + return Task.CompletedTask; + } + + public override void Dispose() + { + this.externalApiPoller?.Dispose(); + } + } + + public static partial class IFullNodeBuilderExtensions + { + public static IFullNodeBuilder AddExternalApi(this IFullNodeBuilder fullNodeBuilder) + { + LoggingConfiguration.RegisterFeatureNamespace("externalapi"); + + fullNodeBuilder.ConfigureFeature(features => + features + .AddFeature() + .FeatureServices(services => services + .AddSingleton() + .AddSingleton() + )); + + return fullNodeBuilder; + } + } +} diff --git a/src/Stratis.Bitcoin.Features.ExternalAPI/ExternalApiPoller.cs b/src/Stratis.Bitcoin.Features.ExternalAPI/ExternalApiPoller.cs new file mode 100644 index 0000000000..e3ef90a42f --- /dev/null +++ b/src/Stratis.Bitcoin.Features.ExternalAPI/ExternalApiPoller.cs @@ -0,0 +1,169 @@ +using System; +using Microsoft.Extensions.Logging; +using Stratis.Bitcoin.AsyncWork; +using Stratis.Bitcoin.Configuration; +using Stratis.Bitcoin.Features.ExternalApi.ApiClients; +using Stratis.Bitcoin.Utilities; + +namespace Stratis.Bitcoin.Features.ExternalApi +{ + public interface IExternalApiPoller : IDisposable + { + void Initialize(); + + decimal GetStratisPrice(); + + decimal GetEthereumPrice(); + + int GetGasPrice(); + + decimal EstimateConversionTransactionGas(); + + decimal EstimateConversionTransactionFee(); + } + + public class ExternalApiPoller : IExternalApiPoller + { + // TODO: This should be linked to the setting in the interop feature + public const int QuorumSize = 6; + + private readonly IAsyncProvider asyncProvider; + private readonly INodeLifetime nodeLifetime; + private readonly ILogger logger; + private readonly ExternalApiSettings externalApiSettings; + private readonly EtherscanClient etherscanClient; + private readonly CoinGeckoClient coinGeckoClient; + + private IAsyncLoop gasPriceLoop; + private IAsyncLoop priceLoop; + + public ExternalApiPoller(NodeSettings nodeSettings, + IAsyncProvider asyncProvider, + INodeLifetime nodeLifetime, + ExternalApiSettings externalApiSettings) + { + this.asyncProvider = asyncProvider; + this.nodeLifetime = nodeLifetime; + this.logger = nodeSettings.LoggerFactory.CreateLogger(this.GetType().FullName); + this.externalApiSettings = externalApiSettings; + this.etherscanClient = new EtherscanClient(this.externalApiSettings); + this.coinGeckoClient = new CoinGeckoClient(this.externalApiSettings); + } + + public void Initialize() + { + this.logger.LogInformation($"External API feature enabled, initializing periodic loops."); + + if (this.externalApiSettings.EthereumGasPriceTracking) + { + this.logger.LogInformation($"Ethereum gas price tracking enabled."); + + this.gasPriceLoop = this.asyncProvider.CreateAndRunAsyncLoop("PeriodicCheckGasPrice", async (cancellation) => + { + this.logger.LogTrace("Beginning gas price check loop."); + + try + { + await this.etherscanClient.GasOracle(true).ConfigureAwait(false); + } + catch (Exception e) + { + this.logger.LogWarning("Exception raised when checking current gas price. {0}", e); + } + + this.logger.LogTrace("Finishing gas price check loop."); + }, + this.nodeLifetime.ApplicationStopping, + repeatEvery: TimeSpans.Minute, + startAfter: TimeSpans.TenSeconds); + } + + if (this.externalApiSettings.PriceTracking) + { + this.logger.LogInformation($"Price tracking for STRAX and ETH enabled."); + + this.priceLoop = this.asyncProvider.CreateAndRunAsyncLoop("PeriodicCheckPrice", async (cancellation) => + { + this.logger.LogTrace("Beginning price check loop."); + + try + { + await this.coinGeckoClient.PriceDataRetrievalAsync().ConfigureAwait(false); + } + catch (Exception e) + { + this.logger.LogWarning("Exception raised when checking current prices. {0}", e); + } + + this.logger.LogTrace("Finishing price check loop."); + }, + this.nodeLifetime.ApplicationStopping, + repeatEvery: TimeSpans.Minute, + startAfter: TimeSpans.TenSeconds); + } + } + + public decimal GetStratisPrice() + { + return this.coinGeckoClient.GetStratisPrice(); + } + + public decimal GetEthereumPrice() + { + return this.coinGeckoClient.GetEthereumPrice(); + } + + public int GetGasPrice() + { + return this.etherscanClient.GetGasPrice(); + } + + /// The decimal type is acceptable here because it supports sufficiently large numbers for most conceivable gas calculations. + /// The estimated total amount of gas a conversion transaction will require. + public decimal EstimateConversionTransactionGas() + { + // The cost of submitting a multisig ERC20 transfer to the multisig contract. + const decimal SubmissionGasCost = 230_000; + + // The cost of submitting a confirmation transaction to the multisig contract. + const decimal ConfirmGasCost = 100_000; + + // The final confirmation that meets the contract threshold; this incurs slightly higher gas due to the transaction execution occurring as well. + const decimal ExecuteGasCost = 160_000; + + // Of the required number of confirmations, one confirmation comes from the initial submission, and the final confirmation is more expensive. + decimal totalGas = SubmissionGasCost + ((QuorumSize - 2) * ConfirmGasCost) + ExecuteGasCost; + + int gasPrice = this.GetGasPrice(); + + return totalGas * gasPrice; + } + + /// The estimated conversion transaction fee, converted from the USD total to the equivalent STRAX amount. + public decimal EstimateConversionTransactionFee() + { + // The approximate USD fee that will be applied to conversion transactions, over and above the computed gas cost. + const decimal ConversionTransactionFee = 100; + + decimal ethereumUsdPrice = this.GetEthereumPrice(); + + if (ethereumUsdPrice == -1) + return -1; + + decimal overallGasUsd = this.EstimateConversionTransactionGas() * ethereumUsdPrice; + + decimal stratisPriceUsd = this.GetStratisPrice(); + + if (stratisPriceUsd == -1) + return -1; + + return (overallGasUsd / stratisPriceUsd) + ConversionTransactionFee; + } + + public void Dispose() + { + this.gasPriceLoop?.Dispose(); + this.priceLoop?.Dispose(); + } + } +} diff --git a/src/Stratis.Bitcoin.Features.ExternalAPI/ExternalApiSettings.cs b/src/Stratis.Bitcoin.Features.ExternalAPI/ExternalApiSettings.cs new file mode 100644 index 0000000000..2aa194c907 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.ExternalAPI/ExternalApiSettings.cs @@ -0,0 +1,31 @@ +using Stratis.Bitcoin.Configuration; + +namespace Stratis.Bitcoin.Features.ExternalApi +{ + public class ExternalApiSettings + { + public const string EtherscanGasOracleUrlKey = "etherscangasoracle"; + + public const string EthereumGasPriceTrackingKey = "ethereumgaspricetracking"; + + public const string PriceUrlKey = "ethereumpriceurl"; + + public const string PriceTrackingKey = "pricetracking"; + + public string EtherscanGasOracleUrl { get; set; } + + public bool EthereumGasPriceTracking { get; set; } + + public string PriceUrl { get; set; } + + public bool PriceTracking { get; set; } + + public ExternalApiSettings(NodeSettings nodeSettings) + { + this.EtherscanGasOracleUrl = nodeSettings.ConfigReader.GetOrDefault(EtherscanGasOracleUrlKey, "https://api.etherscan.io/api?module=gastracker&action=gasoracle"); + this.EthereumGasPriceTracking = nodeSettings.ConfigReader.GetOrDefault(EthereumGasPriceTrackingKey, false); + this.PriceUrl = nodeSettings.ConfigReader.GetOrDefault(PriceUrlKey, "https://api.coingecko.com/api/v3/simple/price?ids=stratis,ethereum&vs_currencies=usd"); + this.PriceTracking = nodeSettings.ConfigReader.GetOrDefault(PriceTrackingKey, false); + } + } +} diff --git a/src/Stratis.Bitcoin.Features.ExternalAPI/Models/CoinGeckoResponse.cs b/src/Stratis.Bitcoin.Features.ExternalAPI/Models/CoinGeckoResponse.cs new file mode 100644 index 0000000000..b6863cc173 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.ExternalAPI/Models/CoinGeckoResponse.cs @@ -0,0 +1,14 @@ +namespace Stratis.Bitcoin.Features.ExternalApi.Models +{ + public class CoinGeckoPriceData + { + public decimal usd { get; set; } + } + + public class CoinGeckoResponse + { + public CoinGeckoPriceData stratis { get; set; } + + public CoinGeckoPriceData ethereum { get; set; } + } +} diff --git a/src/Stratis.Bitcoin.Features.ExternalAPI/Models/EtherscanGasOracleResponse.cs b/src/Stratis.Bitcoin.Features.ExternalAPI/Models/EtherscanGasOracleResponse.cs new file mode 100644 index 0000000000..1bc915796e --- /dev/null +++ b/src/Stratis.Bitcoin.Features.ExternalAPI/Models/EtherscanGasOracleResponse.cs @@ -0,0 +1,22 @@ +namespace Stratis.Bitcoin.Features.ExternalApi.Models +{ + public class EtherscanGasOracleResponse + { + public string status { get; set; } + + public string message { get; set; } + + public EtherscanGasOracleResponseResult result { get; set; } + } + + public class EtherscanGasOracleResponseResult + { + public int LastBlock { get; set; } + + public int SafeGasPrice { get; set; } + + public int ProposeGasPrice { get; set; } + + public int FastGasPrice { get; set; } + } +} diff --git a/src/Stratis.Bitcoin.Features.ExternalAPI/Stratis.Bitcoin.Features.ExternalApi.csproj b/src/Stratis.Bitcoin.Features.ExternalAPI/Stratis.Bitcoin.Features.ExternalApi.csproj new file mode 100644 index 0000000000..85991d58d6 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.ExternalAPI/Stratis.Bitcoin.Features.ExternalApi.csproj @@ -0,0 +1,23 @@ + + + + netcoreapp3.1 + 1.0.8.0 + Stratis Group Ltd. + Stratis.Features.ExternalAPI + Stratis.Features.ExternalAPI + + + + bin\Debug\netcoreapp3.1\Stratis.Bitcoin.Features.ExternalAPI.xml + + + + + + + + + + + diff --git a/src/Stratis.Bitcoin.Features.Interop/InteropPoller.cs b/src/Stratis.Bitcoin.Features.Interop/InteropPoller.cs index b2118ec212..70ccdcd944 100644 --- a/src/Stratis.Bitcoin.Features.Interop/InteropPoller.cs +++ b/src/Stratis.Bitcoin.Features.Interop/InteropPoller.cs @@ -412,7 +412,7 @@ private async Task ProcessConversionRequestsAsync() { await this.BroadcastCoordinationAsync(request.RequestId, transactionId2).ConfigureAwait(false); - BigInteger agreedTransactionId = this.interopTransactionManager.GetAgreedTransactionId(request.RequestId, 6); + BigInteger agreedTransactionId = this.interopTransactionManager.GetAgreedTransactionId(request.RequestId, this.interopSettings.MultisigWalletQuorum); if (agreedTransactionId != BigInteger.MinusOne) { @@ -424,7 +424,7 @@ private async Task ProcessConversionRequestsAsync() break; case ((int)ConversionRequestStatus.VoteFinalised): - BigInteger transactionId3 = this.interopTransactionManager.GetAgreedTransactionId(request.RequestId, 6); + BigInteger transactionId3 = this.interopTransactionManager.GetAgreedTransactionId(request.RequestId, this.interopSettings.MultisigWalletQuorum); if (transactionId3 != BigInteger.MinusOne) { @@ -463,7 +463,7 @@ private async Task ProcessConversionRequestsAsync() // So each node needs to satisfy itself that the transactionId sent by the originator exists in the multisig wallet. // This is done within the InteropBehavior automatically, we just check each poll loop if a transaction has enough votes yet. // Each node must only ever confirm a single transactionId for a given conversion transaction. - BigInteger agreedUponId = this.interopTransactionManager.GetAgreedTransactionId(request.RequestId, 6); + BigInteger agreedUponId = this.interopTransactionManager.GetAgreedTransactionId(request.RequestId, this.interopSettings.MultisigWalletQuorum); if (agreedUponId != BigInteger.MinusOne) { diff --git a/src/Stratis.Bitcoin.Features.Interop/InteropRequest.cs b/src/Stratis.Bitcoin.Features.Interop/InteropRequest.cs new file mode 100644 index 0000000000..c4520e7455 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.Interop/InteropRequest.cs @@ -0,0 +1,99 @@ +using NBitcoin; + +namespace Stratis.Bitcoin.Features.Interop +{ + public enum InteropRequestType + { + // Stratis (Cirrus) network invoking Ethereum contract. + InvokeEthereum = 0, + // Ethereum network invoking Stratis (Cirrus) contract. + InvokeStratis = 1 + } + + /// + /// This class is dual-purposed to store both types of interop requests (either direction), so + /// the meaning of some of the properties varies slightly depending on which direction the request + /// is being made in. + /// + public class InteropRequest : IBitcoinSerializable + { + private string requestId; + + private string transactionId; + + private int requestType; + + private string targetContractAddress; + + private string sourceAddress; + + private string methodName; + + private string[] parameters; + + private bool processed; + + private string response; + + /// + /// The unique identifier for this particular remote contract invocation request. + /// It gets selected by the request creator. + /// + public string RequestId { get { return this.requestId; } set { this.requestId = value; } } + + /// + /// For an interop request for a Stratis contract, this is the txid of the contract call transaction. + /// For an Ethereum invocation this gets populated with the txid on the Ethereum chain of the transaction that was used to store the interop result. + /// + public string TransactionId { get { return this.transactionId; } set { this.transactionId = value; } } + + public int RequestType { get { return this.requestType; } set { this.requestType = value; } } + + /// + /// For an Ethereum invocation, the contract address stores the interop contract on the Stratis network that initiated the request. + /// This is to facilitate the response being stored against the same contract when it is available. + /// For a Stratis invocation, this is the address of the contract to be invoked. + /// + public string TargetContractAddress { get { return this.targetContractAddress; } set { this.targetContractAddress = value; } } + + /// + /// For an Ethereum invocation, this stores the origin of the transaction on the Cirrus network (ultimately this should support both a contract address or a normal address). + /// For a Stratis invocation, this is the address of the contract (or normal account) requesting the invocation on the Ethereum side, if available. + /// + public string SourceAddress { get { return this.sourceAddress; } set { this.sourceAddress = value; } } + + /// + /// For either type of invocation, this is the name of the method that is to be invoked. + /// + public string MethodName { get { return this.methodName; } set { this.methodName = value; } } + + /// + /// For either type of invocation, these are the parameters to be passed to the specified method. + /// + public string[] Parameters { get { return this.parameters; } set { this.parameters = value; } } + + /// + /// Indicates whether or not this request has been processed by the poller. + /// + public bool Processed { get { return this.processed; } set { this.processed = value; } } + + /// + /// The return value of the method requested for invocation. + /// This is defaulted to an empty string for serialization purposes, but this field only has meaning when Processed is true. + /// + public string Response { get { return this.response; } set { this.response = value; } } + + public void ReadWrite(BitcoinStream s) + { + s.ReadWrite(ref this.requestId); + s.ReadWrite(ref this.transactionId); + s.ReadWrite(ref this.requestType); + s.ReadWrite(ref this.targetContractAddress); + s.ReadWrite(ref this.sourceAddress); + s.ReadWrite(ref this.methodName); + s.ReadWrite(ref this.parameters); + s.ReadWrite(ref this.processed); + s.ReadWrite(ref this.response); + } + } +} diff --git a/src/Stratis.Bitcoin.Features.Interop/InteropRequestKeyValueStore.cs b/src/Stratis.Bitcoin.Features.Interop/InteropRequestKeyValueStore.cs new file mode 100644 index 0000000000..6d8bbd1236 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.Interop/InteropRequestKeyValueStore.cs @@ -0,0 +1,136 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; +using LevelDB; +using Stratis.Bitcoin.Configuration; +using Stratis.Bitcoin.Persistence; +using Stratis.Bitcoin.Utilities; +using Stratis.Bitcoin.Utilities.JsonConverters; + +namespace Stratis.Bitcoin.Features.Interop +{ + /// + /// This is implemented separately to so that the repository can live in its own folder on disk. + /// + public interface IInteropRequestKeyValueStore : IKeyValueRepository + { + List GetAll(int type, bool onlyUnprocessed); + } + + public class InteropRequestKeyValueStore : IInteropRequestKeyValueStore + { + /// Access to database. + private readonly DB leveldb; + + private readonly DBreezeSerializer dBreezeSerializer; + + public InteropRequestKeyValueStore(DataFolder dataFolder, DBreezeSerializer dBreezeSerializer) : this(dataFolder.InteropRepositoryPath, dBreezeSerializer) + { + } + + public InteropRequestKeyValueStore(string folder, DBreezeSerializer dBreezeSerializer) + { + Directory.CreateDirectory(folder); + this.dBreezeSerializer = dBreezeSerializer; + + // Open a connection to a new DB and create if not found. + var options = new Options { CreateIfMissing = true }; + this.leveldb = new DB(options, folder); + } + + public List GetAll(int type, bool onlyUnprocessed) + { + var values = new List(); + IEnumerator> enumerator = this.leveldb.GetEnumerator(); + + while (enumerator.MoveNext()) + { + (byte[] key, byte[] value) = enumerator.Current; + + if (value == null) + continue; + + InteropRequest deserialized = this.dBreezeSerializer.Deserialize(value); + + if (deserialized.RequestType != type) + continue; + + if (deserialized.Processed && onlyUnprocessed) + continue; + + values.Add(deserialized); + } + + return values; + } + + /// + public void SaveBytes(string key, byte[] bytes) + { + byte[] keyBytes = Encoding.ASCII.GetBytes(key); + + this.leveldb.Put(keyBytes, bytes); + } + + /// + public void SaveValue(string key, T value) + { + this.SaveBytes(key, this.dBreezeSerializer.Serialize(value)); + } + + /// + public void SaveValueJson(string key, T value) + { + string json = Serializer.ToString(value); + byte[] jsonBytes = Encoding.ASCII.GetBytes(json); + + this.SaveBytes(key, jsonBytes); + } + + /// + public byte[] LoadBytes(string key) + { + byte[] keyBytes = Encoding.ASCII.GetBytes(key); + + byte[] row = this.leveldb.Get(keyBytes); + + if (row == null) + return null; + + return row; + } + + /// + public T LoadValue(string key) + { + byte[] bytes = this.LoadBytes(key); + + if (bytes == null) + return default(T); + + T value = this.dBreezeSerializer.Deserialize(bytes); + return value; + } + + /// + public T LoadValueJson(string key) + { + byte[] bytes = this.LoadBytes(key); + + if (bytes == null) + return default(T); + + string json = Encoding.ASCII.GetString(bytes); + + T value = Serializer.ToObject(json); + + return value; + } + + /// + public void Dispose() + { + this.leveldb.Dispose(); + } + } +} diff --git a/src/Stratis.Bitcoin.Features.Interop/InteropRequestRepository.cs b/src/Stratis.Bitcoin.Features.Interop/InteropRequestRepository.cs new file mode 100644 index 0000000000..848d742465 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.Interop/InteropRequestRepository.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using Microsoft.Extensions.Logging; + +namespace Stratis.Bitcoin.Features.Interop +{ + public interface IInteropRequestRepository + { + void Save(InteropRequest request); + + InteropRequest Get(string requestId); + + List GetAllEthereum(bool onlyUnprocessed); + + List GetAllStratis(bool onlyUnprocessed); + } + + public class InteropRequestRepository : IInteropRequestRepository + { + private const string TableName = "InteropRequests"; + + public IInteropRequestKeyValueStore KeyValueStore { get; } + + private readonly ILogger logger; + + public InteropRequestRepository(ILoggerFactory loggerFactory, IInteropRequestKeyValueStore interopRequestKeyValueStore) + { + this.KeyValueStore = interopRequestKeyValueStore; + + this.logger = loggerFactory.CreateLogger(this.GetType().FullName); + } + + public void Save(InteropRequest request) + { + this.logger.LogDebug($"Saving interop request {request.RequestId} to store."); + + this.KeyValueStore.SaveValue(request.RequestId, request); + } + + public InteropRequest Get(string requestId) + { + this.logger.LogDebug($"Retrieving interop request {requestId} from store."); + + return this.KeyValueStore.LoadValue(requestId); + } + + public List GetAllEthereum(bool onlyUnprocessed) + { + this.logger.LogDebug($"Retrieving all Ethereum interop requests from store, {nameof(onlyUnprocessed)}={onlyUnprocessed}"); + + return this.KeyValueStore.GetAll((int)InteropRequestType.InvokeEthereum, onlyUnprocessed); + } + + public List GetAllStratis(bool onlyUnprocessed) + { + this.logger.LogDebug($"Retrieving all Stratis interop requests from store, {nameof(onlyUnprocessed)}={onlyUnprocessed}"); + + return this.KeyValueStore.GetAll((int)InteropRequestType.InvokeStratis, onlyUnprocessed); + } + } +} diff --git a/src/Stratis.Bitcoin.Features.Interop/InteropSettings.cs b/src/Stratis.Bitcoin.Features.Interop/InteropSettings.cs index 40428e101b..aa76a88552 100644 --- a/src/Stratis.Bitcoin.Features.Interop/InteropSettings.cs +++ b/src/Stratis.Bitcoin.Features.Interop/InteropSettings.cs @@ -9,6 +9,7 @@ public class InteropSettings public const string InteropContractCirrusAddressKey = "interopcontractcirrusaddress"; public const string InteropContractEthereumAddressKey = "interopcontractethereumaddress"; public const string MultisigWalletContractAddressKey = "multisigwalletcontractaddress"; + public const string MultisigWalletContractQuorumKey = "multisigwalletcontractquorum"; public const string WrappedStraxContractAddressKey = "wrappedstraxcontractaddress"; public const string EthereumClientUrlKey = "ethereumclienturl"; public const string EthereumAccountKey = "ethereumaccount"; @@ -24,6 +25,8 @@ public class InteropSettings public string MultisigWalletAddress { get; set; } + public int MultisigWalletQuorum { get; set; } + public string WrappedStraxAddress { get; set; } public string EthereumClientUrl { get; set; } @@ -38,7 +41,7 @@ public class InteropSettings public int EthereumGas { get; set; } /// - /// The gas price for Ethereum interoperability transactions (denominated in gwei). + /// The gas price for Ethereum interoperability transactions (will be converted to gwei). /// public int EthereumGasPrice { get; set; } @@ -50,6 +53,8 @@ public InteropSettings(NodeSettings nodeSettings) this.InteropContractEthereumAddress = nodeSettings.ConfigReader.GetOrDefault(InteropContractEthereumAddressKey, ""); this.MultisigWalletAddress = nodeSettings.ConfigReader.GetOrDefault(MultisigWalletContractAddressKey, ""); + this.MultisigWalletQuorum = nodeSettings.ConfigReader.GetOrDefault(MultisigWalletContractQuorumKey, 6); + this.WrappedStraxAddress = nodeSettings.ConfigReader.GetOrDefault(WrappedStraxContractAddressKey, ""); this.EthereumClientUrl = nodeSettings.ConfigReader.GetOrDefault(EthereumClientUrlKey, "http://localhost:8545"); this.EthereumAccount = nodeSettings.ConfigReader.GetOrDefault(EthereumAccountKey, ""); diff --git a/src/Stratis.Bitcoin.Features.MemoryPool/Rules/CreateMempoolEntryMempoolRule.cs b/src/Stratis.Bitcoin.Features.MemoryPool/Rules/CreateMempoolEntryMempoolRule.cs index d301d4e62c..2acf1e0952 100644 --- a/src/Stratis.Bitcoin.Features.MemoryPool/Rules/CreateMempoolEntryMempoolRule.cs +++ b/src/Stratis.Bitcoin.Features.MemoryPool/Rules/CreateMempoolEntryMempoolRule.cs @@ -234,13 +234,10 @@ private bool AreInputsStandard(Network network, Transaction tx, MempoolCoinView var redeemScript = new Script(ctx.Stack.Top(-1)); // TODO: Move this into a network-specific rule so that it only applies to Strax (the Cirrus validator already allows non-standard transactions) - if (!redeemScript.ToOps().Select(o => o.Code).Contains(OpcodeType.OP_FEDERATION)) + if (redeemScript.GetSigOpCount(true, this.network) > MaxP2SHSigOps) { - if (redeemScript.GetSigOpCount(true) > MaxP2SHSigOps) - { - this.logger.LogTrace("(-)[SIG_OP_MAX]:false"); - return false; - } + this.logger.LogTrace("(-)[SIG_OP_MAX]:false"); + return false; } } } diff --git a/src/Stratis.Bitcoin.Features.PoA/FederationManager.cs b/src/Stratis.Bitcoin.Features.PoA/FederationManager.cs index 8d74ff6eea..43776ffa04 100644 --- a/src/Stratis.Bitcoin.Features.PoA/FederationManager.cs +++ b/src/Stratis.Bitcoin.Features.PoA/FederationManager.cs @@ -139,7 +139,7 @@ public void Initialize() private bool InitializeFederationMemberKey() { - if (!this.nodeSettings.DevMode) + if (this.nodeSettings.DevMode == null) { // Load key. Key key = new KeyTool(this.nodeSettings.DataFolder).LoadPrivateKey(); diff --git a/src/Stratis.Bitcoin.Features.PoA/Models/FederationMemberModel.cs b/src/Stratis.Bitcoin.Features.PoA/Models/FederationMemberModel.cs index 1e7c0855fc..547bfc4fce 100644 --- a/src/Stratis.Bitcoin.Features.PoA/Models/FederationMemberModel.cs +++ b/src/Stratis.Bitcoin.Features.PoA/Models/FederationMemberModel.cs @@ -1,5 +1,4 @@ using System; -using NBitcoin; using Newtonsoft.Json; namespace Stratis.Bitcoin.Features.PoA.Models @@ -7,7 +6,7 @@ namespace Stratis.Bitcoin.Features.PoA.Models public class FederationMemberModel { [JsonProperty("pubkey")] - public PubKey PubKey { get; set; } + public string PubKey { get; set; } [JsonProperty("collateralAmount")] public decimal CollateralAmount { get; set; } diff --git a/src/Stratis.Bitcoin.Features.PoA/PoAFeature.cs b/src/Stratis.Bitcoin.Features.PoA/PoAFeature.cs index ad3c3adbe5..502ab49da7 100644 --- a/src/Stratis.Bitcoin.Features.PoA/PoAFeature.cs +++ b/src/Stratis.Bitcoin.Features.PoA/PoAFeature.cs @@ -136,7 +136,10 @@ public override Task InitializeAsync() if (rebuildFederationHeight) this.reconstructFederationService.Reconstruct(); - this.miner?.InitializeMining(); + // If the node is started in devmode, its role must be of miner in order to mine. + // If devmode is not specified, initialize mining as per normal. + if (this.nodeSettings.DevMode == null || this.nodeSettings.DevMode == DevModeNodeRole.Miner) + this.miner?.InitializeMining(); return Task.CompletedTask; } diff --git a/src/Stratis.Bitcoin.Features.PoA/PoAMiner.cs b/src/Stratis.Bitcoin.Features.PoA/PoAMiner.cs index 1a977086ab..328c047028 100644 --- a/src/Stratis.Bitcoin.Features.PoA/PoAMiner.cs +++ b/src/Stratis.Bitcoin.Features.PoA/PoAMiner.cs @@ -190,15 +190,18 @@ private async Task CreateBlocksAsync() var builder = new StringBuilder(); builder.AppendLine("<<==============================================================>>"); builder.AppendLine($"Block mined hash : '{chainedHeader}'"); - builder.AppendLine($"Block miner pubkey : '{this.federationManager.CurrentFederationKey.PubKey.ToString()}'"); + builder.AppendLine($"Block miner pubkey : '{this.federationManager.CurrentFederationKey.PubKey}'"); builder.AppendLine("<<==============================================================>>"); this.logger.LogInformation(builder.ToString()); + // If DevMode is enabled the miner will continue it's bootstrapped mining, i.e. without any connections. + if ((this.nodeSettings.DevMode != null && this.nodeSettings.DevMode == DevModeNodeRole.Miner)) + continue; + // The purpose of bootstrap mode is to kickstart the network when the last mined block is very old, which would normally put the node in IBD and inhibit mining. // There is therefore no point keeping this mode enabled once this node has mined successfully. // Additionally, keeping it enabled may result in network splits if this node becomes disconnected from its peers for a prolonged period. - // If DevMode is enabled the miner will conitnue it's bootstrapped mining, i.e. without any connections. - if (this.poaSettings.BootstrappingMode && !this.nodeSettings.DevMode) + if (this.poaSettings.BootstrappingMode) { this.logger.LogInformation("Disabling bootstrap mode as a block has been successfully mined."); this.poaSettings.DisableBootstrap(); @@ -424,7 +427,7 @@ private void AddComponentStats(StringBuilder log) // If the node is in DevMode just use the genesis members via the federation manager. List modifiedFederation; - if (this.nodeSettings.DevMode) + if (this.nodeSettings.DevMode != null) modifiedFederation = this.federationManager.GetFederationMembers(); else modifiedFederation = this.votingManager?.GetModifiedFederation(currentHeader) ?? this.federationManager.GetFederationMembers(); @@ -456,7 +459,7 @@ private void AddComponentStats(StringBuilder log) currentHeader = currentHeader.Previous; hitCount++; - if (this.nodeSettings.DevMode) + if (this.nodeSettings.DevMode != null) modifiedFederation = this.federationManager.GetFederationMembers(); else modifiedFederation = this.votingManager?.GetModifiedFederation(currentHeader) ?? this.federationManager.GetFederationMembers(); diff --git a/src/Stratis.Bitcoin.Features.PoA/Voting/FederationController.cs b/src/Stratis.Bitcoin.Features.PoA/Voting/FederationController.cs index e07bb5731d..bc861e5634 100644 --- a/src/Stratis.Bitcoin.Features.PoA/Voting/FederationController.cs +++ b/src/Stratis.Bitcoin.Features.PoA/Voting/FederationController.cs @@ -83,10 +83,10 @@ public IActionResult GetCurrentMemberInfo() var federationMemberModel = new FederationMemberDetailedModel { - PubKey = this.federationManager.CurrentFederationKey.PubKey + PubKey = this.federationManager.CurrentFederationKey.PubKey.ToHex() }; - KeyValuePair lastActive = this.idleFederationMembersKicker.GetFederationMembersByLastActiveTime().FirstOrDefault(x => x.Key == federationMemberModel.PubKey); + KeyValuePair lastActive = this.idleFederationMembersKicker.GetFederationMembersByLastActiveTime().FirstOrDefault(x => x.Key == this.federationManager.CurrentFederationKey.PubKey); if (lastActive.Key != null) { federationMemberModel.LastActiveTime = new DateTime(1970, 1, 1, 0, 0, 0).AddSeconds(lastActive.Value); @@ -94,7 +94,7 @@ public IActionResult GetCurrentMemberInfo() } // Is this member part of a pending poll - Poll poll = this.votingManager.GetPendingPolls().MemberPolls().OrderByDescending(p => p.PollStartBlockData.Height).FirstOrDefault(p => this.votingManager.GetMemberVotedOn(p.VotingData).PubKey == federationMemberModel.PubKey); + Poll poll = this.votingManager.GetPendingPolls().MemberPolls().OrderByDescending(p => p.PollStartBlockData.Height).FirstOrDefault(p => this.votingManager.GetMemberVotedOn(p.VotingData).PubKey == this.federationManager.CurrentFederationKey.PubKey); if (poll != null) { federationMemberModel.PollType = poll.VotingData.Key.ToString(); @@ -103,7 +103,7 @@ public IActionResult GetCurrentMemberInfo() } // Has the poll finished? - poll = this.votingManager.GetApprovedPolls().MemberPolls().OrderByDescending(p => p.PollVotedInFavorBlockData.Height).FirstOrDefault(p => this.votingManager.GetMemberVotedOn(p.VotingData).PubKey == federationMemberModel.PubKey); + poll = this.votingManager.GetApprovedPolls().MemberPolls().OrderByDescending(p => p.PollVotedInFavorBlockData.Height).FirstOrDefault(p => this.votingManager.GetMemberVotedOn(p.VotingData).PubKey == this.federationManager.CurrentFederationKey.PubKey); if (poll != null) { federationMemberModel.PollType = poll.VotingData.Key.ToString(); @@ -120,7 +120,7 @@ public IActionResult GetCurrentMemberInfo() } // Has the poll executed? - poll = this.votingManager.GetExecutedPolls().MemberPolls().OrderByDescending(p => p.PollExecutedBlockData.Height).FirstOrDefault(p => this.votingManager.GetMemberVotedOn(p.VotingData).PubKey == federationMemberModel.PubKey); + poll = this.votingManager.GetExecutedPolls().MemberPolls().OrderByDescending(p => p.PollExecutedBlockData.Height).FirstOrDefault(p => this.votingManager.GetMemberVotedOn(p.VotingData).PubKey == this.federationManager.CurrentFederationKey.PubKey); if (poll != null) federationMemberModel.PollExecutedBlockHeight = poll.PollExecutedBlockData.Height; @@ -159,7 +159,7 @@ public IActionResult GetMembers() { federationMemberModels.Add(new FederationMemberModel() { - PubKey = federationMember.PubKey, + PubKey = federationMember.PubKey.ToHex(), CollateralAmount = (federationMember as CollateralFederationMember).CollateralAmount.ToUnit(MoneyUnit.BTC), LastActiveTime = new DateTime(1970, 1, 1, 0, 0, 0).AddSeconds(activeTimes.FirstOrDefault(a => a.Key == federationMember.PubKey).Value), PeriodOfInActivity = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0).AddSeconds(activeTimes.FirstOrDefault(a => a.Key == federationMember.PubKey).Value) diff --git a/src/Stratis.Bitcoin.Features.SmartContracts/ReflectionExecutor/Controllers/SmartContractsController.cs b/src/Stratis.Bitcoin.Features.SmartContracts/ReflectionExecutor/Controllers/SmartContractsController.cs index c2a74a856e..ad10848c53 100644 --- a/src/Stratis.Bitcoin.Features.SmartContracts/ReflectionExecutor/Controllers/SmartContractsController.cs +++ b/src/Stratis.Bitcoin.Features.SmartContracts/ReflectionExecutor/Controllers/SmartContractsController.cs @@ -517,7 +517,7 @@ public async Task BuildAndSendCreateSmartContractTransactionAsync return ModelStateErrors.BuildErrorResponse(this.ModelState); // Ignore this check if the node is running dev mode. - if (!this.nodeSettings.DevMode && !this.connectionManager.ConnectedPeers.Any()) + if (this.nodeSettings.DevMode == null && !this.connectionManager.ConnectedPeers.Any()) { this.logger.LogTrace("(-)[NO_CONNECTED_PEERS]"); return ErrorHelpers.BuildErrorResponse(HttpStatusCode.Forbidden, "Can't send transaction as the node requires at least one connection.", string.Empty); @@ -570,7 +570,7 @@ public async Task BuildAndSendCallSmartContractTransactionAsync([ return ModelStateErrors.BuildErrorResponse(this.ModelState); // Ignore this check if the node is running dev mode. - if (!this.nodeSettings.DevMode && !this.connectionManager.ConnectedPeers.Any()) + if (this.nodeSettings.DevMode == null && !this.connectionManager.ConnectedPeers.Any()) { this.logger.LogTrace("(-)[NO_CONNECTED_PEERS]"); return ErrorHelpers.BuildErrorResponse(HttpStatusCode.Forbidden, "Can't send transaction as the node requires at least one connection.", string.Empty); diff --git a/src/Stratis.Bitcoin.Features.Wallet.Tests/WalletControllerTest.cs b/src/Stratis.Bitcoin.Features.Wallet.Tests/WalletControllerTest.cs index af2de8b787..b230503ba1 100644 --- a/src/Stratis.Bitcoin.Features.Wallet.Tests/WalletControllerTest.cs +++ b/src/Stratis.Bitcoin.Features.Wallet.Tests/WalletControllerTest.cs @@ -704,7 +704,7 @@ public async Task GetGeneralInfoSuccessfullyReturnsWalletGeneralInfoModel() var viewResult = Assert.IsType(result); var resultValue = Assert.IsType(viewResult.Value); - Assert.Equal(wallet.Network, resultValue.Network); + Assert.Equal(wallet.Network.Name, resultValue.Network); Assert.Equal(wallet.CreationTime, resultValue.CreationTime); Assert.Equal(15, resultValue.LastBlockSyncedHeight); Assert.Equal(0, resultValue.ConnectedNodes); diff --git a/src/Stratis.Bitcoin.Features.Wallet/Models/RequestModels.cs b/src/Stratis.Bitcoin.Features.Wallet/Models/RequestModels.cs index 6aa3e88eaa..b8f0362684 100644 --- a/src/Stratis.Bitcoin.Features.Wallet/Models/RequestModels.cs +++ b/src/Stratis.Bitcoin.Features.Wallet/Models/RequestModels.cs @@ -89,7 +89,6 @@ public class WalletLoadRequest : RequestModel /// public class WalletRecoveryRequest : RequestModel { - /// /// The mnemonic that was used to create the wallet. /// diff --git a/src/Stratis.Bitcoin.Features.Wallet/Models/WalletGeneralInfoModel.cs b/src/Stratis.Bitcoin.Features.Wallet/Models/WalletGeneralInfoModel.cs index 972c2ab689..e88d4621d9 100644 --- a/src/Stratis.Bitcoin.Features.Wallet/Models/WalletGeneralInfoModel.cs +++ b/src/Stratis.Bitcoin.Features.Wallet/Models/WalletGeneralInfoModel.cs @@ -1,5 +1,4 @@ using System; -using NBitcoin; using Newtonsoft.Json; using Stratis.Bitcoin.Utilities.JsonConverters; @@ -14,8 +13,7 @@ public class WalletGeneralInfoModel public string WalletName { get; set; } [JsonProperty(PropertyName = "network")] - [JsonConverter(typeof(NetworkConverter))] - public Network Network { get; set; } + public string Network { get; set; } /// /// The time this wallet was created. diff --git a/src/Stratis.Bitcoin.Features.Wallet/Services/WalletService.cs b/src/Stratis.Bitcoin.Features.Wallet/Services/WalletService.cs index 8fbf18cfed..7b9b945100 100644 --- a/src/Stratis.Bitcoin.Features.Wallet/Services/WalletService.cs +++ b/src/Stratis.Bitcoin.Features.Wallet/Services/WalletService.cs @@ -134,7 +134,7 @@ public async Task GetWalletGeneralInfo(string walletName return new WalletGeneralInfoModel { WalletName = wallet.Name, - Network = wallet.Network, + Network = wallet.Network.Name, CreationTime = wallet.CreationTime, LastBlockSyncedHeight = wallet.AccountsRoot.Single().LastBlockSyncedHeight, ConnectedNodes = this.connectionManager.ConnectedPeers.Count(), @@ -491,7 +491,7 @@ public async Task SendTransaction(SendTransactionReq { return await Task.Run(() => { - if (!this.nodeSettings.DevMode && !this.connectionManager.ConnectedPeers.Any()) + if (this.nodeSettings.DevMode == null && !this.connectionManager.ConnectedPeers.Any()) { this.logger.LogTrace("(-)[NO_CONNECTED_PEERS]"); diff --git a/src/Stratis.Bitcoin.IntegrationTests/API/ApiSteps.cs b/src/Stratis.Bitcoin.IntegrationTests/API/ApiSteps.cs index 401e6a1656..04943f0f28 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/API/ApiSteps.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/API/ApiSteps.cs @@ -452,7 +452,7 @@ private void general_information_about_the_wallet_and_node_is_returned() { var generalInfoResponse = JsonDataSerializer.Instance.Deserialize(this.responseText); generalInfoResponse.WalletName.Should().Be(WalletName); - generalInfoResponse.Network.Name.Should().Be("StraxRegTest"); + generalInfoResponse.Network.Should().Be("StraxRegTest"); generalInfoResponse.ChainTip.Should().Be(0); generalInfoResponse.IsChainSynced.Should().BeFalse(); generalInfoResponse.ConnectedNodes.Should().Be(0); diff --git a/src/Stratis.Bitcoin.IntegrationTests/Wallet/WalletOperationsTests.cs b/src/Stratis.Bitcoin.IntegrationTests/Wallet/WalletOperationsTests.cs index 5fe34f2930..72acd671e4 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/Wallet/WalletOperationsTests.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/Wallet/WalletOperationsTests.cs @@ -1223,7 +1223,7 @@ public async Task GetWalletGeneralInfoAsync() generalInfoModel.ConnectedNodes.Should().Be(0); generalInfoModel.CreationTime.ToUnixTimeSeconds().Should().Be(1470467001); generalInfoModel.IsDecrypted.Should().BeTrue(); - generalInfoModel.Network.Name.Should().Be(new StraxRegTest().Name); + generalInfoModel.Network.Should().Be(new StraxRegTest().Name); //generalInfoModel.WalletFilePath.Should().Be(this.fixture.WalletWithFundsFilePath); } diff --git a/src/Stratis.Bitcoin.Networks/StraxMain.cs b/src/Stratis.Bitcoin.Networks/StraxMain.cs index e9e9b8d71d..b2832e3414 100644 --- a/src/Stratis.Bitcoin.Networks/StraxMain.cs +++ b/src/Stratis.Bitcoin.Networks/StraxMain.cs @@ -37,6 +37,8 @@ public StraxMain() this.RewardClaimerBatchActivationHeight = 119_200; // Tuesday, 12 January 2021 9:00:00 AM (Estimated) this.RewardClaimerBlockInterval = 100; + this.ConversionTransactionFeeDistributionDummyAddress = "CXK1AhmK8XhmBWHUrCKRt5WMhz1CcYeguF"; + // To successfully process the OP_FEDERATION opcode the federations should be known. this.Federations = new Federations(); this.Federations.RegisterFederation(new Federation(new[] diff --git a/src/Stratis.Bitcoin.Networks/StraxRegTest.cs b/src/Stratis.Bitcoin.Networks/StraxRegTest.cs index fd016b482d..bd3b50ad3b 100644 --- a/src/Stratis.Bitcoin.Networks/StraxRegTest.cs +++ b/src/Stratis.Bitcoin.Networks/StraxRegTest.cs @@ -36,6 +36,8 @@ public StraxRegTest() this.CirrusRewardDummyAddress = "PDpvfcpPm9cjQEoxWzQUL699N8dPaf8qML"; // Cirrus test address + this.ConversionTransactionFeeDistributionDummyAddress = "PTCPsLQoF3WNoH1qXMy5PouquiXQKp7WBV"; + var powLimit = new Target(new uint256("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")); var consensusFactory = new PosConsensusFactory(); diff --git a/src/Stratis.Bitcoin.Networks/StraxTest.cs b/src/Stratis.Bitcoin.Networks/StraxTest.cs index d4d6e8340f..97fd0dac0d 100644 --- a/src/Stratis.Bitcoin.Networks/StraxTest.cs +++ b/src/Stratis.Bitcoin.Networks/StraxTest.cs @@ -38,6 +38,8 @@ public StraxTest() this.RewardClaimerBatchActivationHeight = 166200; this.RewardClaimerBlockInterval = 100; + this.ConversionTransactionFeeDistributionDummyAddress = "tUAzRBe1CaKaZnrxWPLVv7F4owHHKXAtbj"; + var consensusFactory = new PosConsensusFactory(); // Create the genesis block. diff --git a/src/Stratis.Bitcoin.Tests/Controllers/NodeControllerTest.cs b/src/Stratis.Bitcoin.Tests/Controllers/NodeControllerTest.cs index e728186da9..791e140685 100644 --- a/src/Stratis.Bitcoin.Tests/Controllers/NodeControllerTest.cs +++ b/src/Stratis.Bitcoin.Tests/Controllers/NodeControllerTest.cs @@ -34,6 +34,7 @@ public class NodeControllerTest : LogsTestBase private readonly Mock connectionManager; private readonly Mock dateTimeProvider; private readonly Mock fullNode; + private readonly Mock initialBlockDownloadState; private readonly Mock peerBanning; private readonly Network network; private readonly NodeSettings nodeSettings; @@ -59,6 +60,7 @@ public NodeControllerTest() this.connectionManager.Setup(c => c.Network).Returns(this.network); this.dateTimeProvider = new Mock(); this.fullNode = new Mock(); + this.initialBlockDownloadState = new Mock(this.chainState.Object, this.network, null, new Checkpoints(), DateTimeProvider.Default); this.nodeSettings = new NodeSettings(networksSelector: Networks.Networks.Bitcoin); this.peerBanning = new Mock(); @@ -84,6 +86,7 @@ public NodeControllerTest() this.selfEndpointTracker.Object, this.consensusManager.Object, this.blockStore.Object, + this.initialBlockDownloadState.Object, this.getUnspentTransaction.Object, this.networkDifficulty.Object, this.pooledGetUnspentTransaction.Object, @@ -131,8 +134,8 @@ public async Task GetRawTransactionAsync_TransactionCannotBeFound_ReturnsNullAsy .Verifiable(); this.controller = new NodeController(this.chainIndexer, this.chainState.Object, this.connectionManager.Object, this.dateTimeProvider.Object, this.fullNode.Object, - this.LoggerFactory.Object, this.nodeSettings, this.network, this.asyncProvider.Object, this.selfEndpointTracker.Object, this.consensusManager.Object, this.blockStore.Object, this.getUnspentTransaction.Object, - this.networkDifficulty.Object, this.pooledGetUnspentTransaction.Object, this.pooledTransaction.Object); + this.LoggerFactory.Object, this.nodeSettings, this.network, this.asyncProvider.Object, this.selfEndpointTracker.Object, this.consensusManager.Object, this.blockStore.Object, this.initialBlockDownloadState.Object, + this.getUnspentTransaction.Object, this.networkDifficulty.Object, this.pooledGetUnspentTransaction.Object, this.pooledTransaction.Object); string txid = txId.ToString(); bool verbose = false; @@ -155,8 +158,8 @@ public async Task GetRawTransactionAsync_TransactionNotInPooledTransaction_Retur .Returns(transaction); this.controller = new NodeController(this.chainIndexer, this.chainState.Object, this.connectionManager.Object, this.dateTimeProvider.Object, this.fullNode.Object, - this.LoggerFactory.Object, this.nodeSettings, this.network, this.asyncProvider.Object, this.selfEndpointTracker.Object, this.consensusManager.Object, this.blockStore.Object, this.getUnspentTransaction.Object, - this.networkDifficulty.Object, this.pooledGetUnspentTransaction.Object, this.pooledTransaction.Object); + this.LoggerFactory.Object, this.nodeSettings, this.network, this.asyncProvider.Object, this.selfEndpointTracker.Object, this.consensusManager.Object, this.blockStore.Object, this.initialBlockDownloadState.Object, + this.getUnspentTransaction.Object, this.networkDifficulty.Object, this.pooledGetUnspentTransaction.Object, this.pooledTransaction.Object); string txid = txId.ToString(); bool verbose = false; @@ -194,8 +197,8 @@ public async Task GetRawTransactionAsync_PooledTransactionServiceNotAvailable_Re .Returns(transaction); this.controller = new NodeController(this.chainIndexer, this.chainState.Object, this.connectionManager.Object, this.dateTimeProvider.Object, this.fullNode.Object, - this.LoggerFactory.Object, this.nodeSettings, this.network, this.asyncProvider.Object, this.selfEndpointTracker.Object, this.consensusManager.Object, this.blockStore.Object, this.getUnspentTransaction.Object, - this.networkDifficulty.Object, this.pooledGetUnspentTransaction.Object, this.pooledTransaction.Object); + this.LoggerFactory.Object, this.nodeSettings, this.network, this.asyncProvider.Object, this.selfEndpointTracker.Object, this.consensusManager.Object, this.blockStore.Object, this.initialBlockDownloadState.Object, + this.getUnspentTransaction.Object, this.networkDifficulty.Object, this.pooledGetUnspentTransaction.Object, this.pooledTransaction.Object); string txid = txId.ToString(); bool verbose = false; @@ -215,8 +218,8 @@ public async Task GetRawTransactionAsync_PooledTransactionAndBlockStoreServiceNo .Verifiable(); this.controller = new NodeController(this.chainIndexer, this.chainState.Object, this.connectionManager.Object, this.dateTimeProvider.Object, this.fullNode.Object, - this.LoggerFactory.Object, this.nodeSettings, this.network, this.asyncProvider.Object, this.selfEndpointTracker.Object, this.consensusManager.Object, this.blockStore.Object, this.getUnspentTransaction.Object, - this.networkDifficulty.Object, this.pooledGetUnspentTransaction.Object, this.pooledTransaction.Object); + this.LoggerFactory.Object, this.nodeSettings, this.network, this.asyncProvider.Object, this.selfEndpointTracker.Object, this.consensusManager.Object, this.blockStore.Object, this.initialBlockDownloadState.Object, + this.getUnspentTransaction.Object, this.networkDifficulty.Object, this.pooledGetUnspentTransaction.Object, this.pooledTransaction.Object); string txid = txId.ToString(); bool verbose = false; @@ -236,8 +239,8 @@ public void DecodeRawTransaction_ReturnsTransaction() this.controller = new NodeController(this.chainIndexer, this.chainState.Object, this.connectionManager.Object, this.dateTimeProvider.Object, this.fullNode.Object, - this.LoggerFactory.Object, this.nodeSettings, this.network, this.asyncProvider.Object, this.selfEndpointTracker.Object, this.consensusManager.Object, this.blockStore.Object, this.getUnspentTransaction.Object, - this.networkDifficulty.Object, this.pooledGetUnspentTransaction.Object, this.pooledTransaction.Object); + this.LoggerFactory.Object, this.nodeSettings, this.network, this.asyncProvider.Object, this.selfEndpointTracker.Object, this.consensusManager.Object, this.blockStore.Object, this.initialBlockDownloadState.Object, + this.getUnspentTransaction.Object, this.networkDifficulty.Object, this.pooledGetUnspentTransaction.Object, this.pooledTransaction.Object); var json = (JsonResult)this.controller.DecodeRawTransaction(new DecodeRawTransactionModel() { RawHex = transaction.ToHex() }); var resultModel = (TransactionVerboseModel)json.Value; @@ -405,8 +408,8 @@ public async Task GetTxOutAsync_NotIncludeInMempool_GetUnspentTransactionNotAvai var txId = new uint256(1243124); this.controller = new NodeController(this.chainIndexer, this.chainState.Object, this.connectionManager.Object, this.dateTimeProvider.Object, this.fullNode.Object, - this.LoggerFactory.Object, this.nodeSettings, this.network, this.asyncProvider.Object, this.selfEndpointTracker.Object, this.consensusManager.Object, this.blockStore.Object, this.getUnspentTransaction.Object, - this.networkDifficulty.Object, this.pooledGetUnspentTransaction.Object, this.pooledTransaction.Object); + this.LoggerFactory.Object, this.nodeSettings, this.network, this.asyncProvider.Object, this.selfEndpointTracker.Object, this.consensusManager.Object, this.blockStore.Object, this.initialBlockDownloadState.Object, + this.getUnspentTransaction.Object, this.networkDifficulty.Object, this.pooledGetUnspentTransaction.Object, this.pooledTransaction.Object); string txid = txId.ToString(); uint vout = 0; bool includeMemPool = false; @@ -425,8 +428,8 @@ public async Task GetTxOutAsync_IncludeMempool_UnspentTransactionNotFound_Return .Verifiable(); this.controller = new NodeController(this.chainIndexer, this.chainState.Object, this.connectionManager.Object, this.dateTimeProvider.Object, this.fullNode.Object, - this.LoggerFactory.Object, this.nodeSettings, this.network, this.asyncProvider.Object, this.selfEndpointTracker.Object, this.consensusManager.Object, this.blockStore.Object, this.getUnspentTransaction.Object, - this.networkDifficulty.Object, this.pooledGetUnspentTransaction.Object, this.pooledTransaction.Object); + this.LoggerFactory.Object, this.nodeSettings, this.network, this.asyncProvider.Object, this.selfEndpointTracker.Object, this.consensusManager.Object, this.blockStore.Object, this.initialBlockDownloadState.Object, + this.getUnspentTransaction.Object, this.networkDifficulty.Object, this.pooledGetUnspentTransaction.Object, this.pooledTransaction.Object); string txid = txId.ToString(); uint vout = 0; bool includeMemPool = true; @@ -443,8 +446,8 @@ public async Task GetTxOutAsync_IncludeMempool_PooledGetUnspentTransactionNotAva var txId = new uint256(1243124); this.controller = new NodeController(this.chainIndexer, this.chainState.Object, this.connectionManager.Object, this.dateTimeProvider.Object, this.fullNode.Object, - this.LoggerFactory.Object, this.nodeSettings, this.network, this.asyncProvider.Object, this.selfEndpointTracker.Object, this.consensusManager.Object, this.blockStore.Object, this.getUnspentTransaction.Object, - this.networkDifficulty.Object, this.pooledGetUnspentTransaction.Object, this.pooledTransaction.Object); + this.LoggerFactory.Object, this.nodeSettings, this.network, this.asyncProvider.Object, this.selfEndpointTracker.Object, this.consensusManager.Object, this.blockStore.Object, this.initialBlockDownloadState.Object, + this.getUnspentTransaction.Object, this.networkDifficulty.Object, this.pooledGetUnspentTransaction.Object, this.pooledTransaction.Object); string txid = txId.ToString(); uint vout = 0; bool includeMemPool = true; @@ -465,8 +468,8 @@ public async Task GetTxOutAsync_NotIncludeInMempool_UnspentTransactionFound_Retu .Verifiable(); this.controller = new NodeController(this.chainIndexer, this.chainState.Object, this.connectionManager.Object, this.dateTimeProvider.Object, this.fullNode.Object, - this.LoggerFactory.Object, this.nodeSettings, this.network, this.asyncProvider.Object, this.selfEndpointTracker.Object, this.consensusManager.Object, this.blockStore.Object, this.getUnspentTransaction.Object, - this.networkDifficulty.Object, this.pooledGetUnspentTransaction.Object, this.pooledTransaction.Object); + this.LoggerFactory.Object, this.nodeSettings, this.network, this.asyncProvider.Object, this.selfEndpointTracker.Object, this.consensusManager.Object, this.blockStore.Object, this.initialBlockDownloadState.Object, + this.getUnspentTransaction.Object, this.networkDifficulty.Object, this.pooledGetUnspentTransaction.Object, this.pooledTransaction.Object); string txid = txId.ToString(); uint vout = 0; bool includeMemPool = false; @@ -493,8 +496,8 @@ public async Task GetTxOutAsync_IncludeInMempool_UnspentTransactionFound_Returns .Verifiable(); this.controller = new NodeController(this.chainIndexer, this.chainState.Object, this.connectionManager.Object, this.dateTimeProvider.Object, this.fullNode.Object, - this.LoggerFactory.Object, this.nodeSettings, this.network, this.asyncProvider.Object, this.selfEndpointTracker.Object, this.consensusManager.Object, this.blockStore.Object, this.getUnspentTransaction.Object, - this.networkDifficulty.Object, this.pooledGetUnspentTransaction.Object, this.pooledTransaction.Object); + this.LoggerFactory.Object, this.nodeSettings, this.network, this.asyncProvider.Object, this.selfEndpointTracker.Object, this.consensusManager.Object, this.blockStore.Object, this.initialBlockDownloadState.Object, + this.getUnspentTransaction.Object, this.networkDifficulty.Object, this.pooledGetUnspentTransaction.Object, this.pooledTransaction.Object); string txid = txId.ToString(); uint vout = 0; bool includeMemPool = true; diff --git a/src/Stratis.Bitcoin/Base/InitialBlockDownloadState.cs b/src/Stratis.Bitcoin/Base/InitialBlockDownloadState.cs index 04e28dd6dc..836ddff24a 100644 --- a/src/Stratis.Bitcoin/Base/InitialBlockDownloadState.cs +++ b/src/Stratis.Bitcoin/Base/InitialBlockDownloadState.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.Logging; -using NBitcoin; +using NBitcoin; using Stratis.Bitcoin.Configuration.Settings; using Stratis.Bitcoin.Consensus; using Stratis.Bitcoin.Interfaces; @@ -22,9 +21,6 @@ public class InitialBlockDownloadState : IInitialBlockDownloadState /// Information about node's chain. private readonly IChainState chainState; - /// Instance logger. - private readonly ILogger logger; - /// Specification of the network the node runs on - regtest/testnet/mainnet. private readonly Network network; @@ -34,7 +30,7 @@ public class InitialBlockDownloadState : IInitialBlockDownloadState private int lastCheckpointHeight; private uint256 minimumChainWork; - public InitialBlockDownloadState(IChainState chainState, Network network, ConsensusSettings consensusSettings, ICheckpoints checkpoints, ILoggerFactory loggerFactory, IDateTimeProvider dateTimeProvider) + public InitialBlockDownloadState(IChainState chainState, Network network, ConsensusSettings consensusSettings, ICheckpoints checkpoints, IDateTimeProvider dateTimeProvider) { Guard.NotNull(chainState, nameof(chainState)); @@ -46,8 +42,6 @@ public InitialBlockDownloadState(IChainState chainState, Network network, Consen this.lastCheckpointHeight = this.checkpoints.GetLastCheckpointHeight(); this.minimumChainWork = this.network.Consensus.MinimumChainWork ?? uint256.Zero; - - this.logger = loggerFactory.CreateLogger(this.GetType().FullName); } /// diff --git a/src/Stratis.Bitcoin/Builder/FullNodeFeatureExecutor.cs b/src/Stratis.Bitcoin/Builder/FullNodeFeatureExecutor.cs index e891df35aa..dcbe634669 100644 --- a/src/Stratis.Bitcoin/Builder/FullNodeFeatureExecutor.cs +++ b/src/Stratis.Bitcoin/Builder/FullNodeFeatureExecutor.cs @@ -57,11 +57,11 @@ public void Initialize() this.Execute(feature => { - this.signals.Publish(new FullNodeEvent() { Message = $"Initializing feature '{feature.GetType().Name}' ... " }); + this.signals.Publish(new FullNodeEvent() { Message = $"Initializing feature '{feature.GetType().Name}'.", State = FullNodeState.Initializing.ToString() }); feature.State = FeatureInitializationState.Initializing; feature.InitializeAsync().GetAwaiter().GetResult(); feature.State = FeatureInitializationState.Initialized; - this.signals.Publish(new FullNodeEvent() { Message = $"Feature '{feature.GetType().Name}' initialized ... " }); + this.signals.Publish(new FullNodeEvent() { Message = $"Feature '{feature.GetType().Name}' initialized.", State = FullNodeState.Initializing.ToString() }); }); } catch @@ -164,7 +164,7 @@ private void LogAndAddException(IFullNodeFeature feature, bool disposing, List /// - public bool DevMode { get; private set; } + public DevModeNodeRole? DevMode { get; private set; } /// /// Initializes a new instance of the object. @@ -250,7 +250,16 @@ public NodeSettings(Network network = null, ProtocolVersion protocolVersion = Su this.LoadConfiguration(); // Set the devmode flag. - this.DevMode = this.ConfigReader.GetOrDefault(DevModeParam, false); + var devmode = this.ConfigReader.GetOrDefault(DevModeParam, null); + if (devmode != null) + { + if (devmode == DevModeNodeRole.Default.ToString().ToLowerInvariant()) + this.DevMode = DevModeNodeRole.Default; + else if (devmode == DevModeNodeRole.Miner.ToString().ToLowerInvariant()) + this.DevMode = DevModeNodeRole.Miner; + else + throw new ConfigurationException("Invalid devmode option specified (either 'default' or 'miner' permitted."); + } } /// Determines whether to print help and exit. @@ -441,4 +450,20 @@ public void Dispose() this.LoggerFactory.Dispose(); } } + + /// + /// Determines the type of node that will be started in developer (dev) mode. + /// + public enum DevModeNodeRole + { + /// + /// This role allows the node to join the dev mode network as a miner (currently only one miner is allowed). + /// + Miner, + + /// + /// This role specifies that the node will join the network as a non-miner (normal node). + /// + Default + } } \ No newline at end of file diff --git a/src/Stratis.Bitcoin/Controllers/Models/StatusModel.cs b/src/Stratis.Bitcoin/Controllers/Models/StatusModel.cs index 4a8259a934..effaf15628 100644 --- a/src/Stratis.Bitcoin/Controllers/Models/StatusModel.cs +++ b/src/Stratis.Bitcoin/Controllers/Models/StatusModel.cs @@ -78,6 +78,9 @@ public StatusModel() /// Returns the status of the node. public string State { get; set; } + + /// Returns whether or not the node is in Initial Block Download (syncing). + public bool? InIbd { get; set; } } /// diff --git a/src/Stratis.Bitcoin/FullNode.cs b/src/Stratis.Bitcoin/FullNode.cs index 25246de66d..11f3b32cee 100644 --- a/src/Stratis.Bitcoin/FullNode.cs +++ b/src/Stratis.Bitcoin/FullNode.cs @@ -190,6 +190,7 @@ public IFullNode Initialize(IFullNodeServiceProvider serviceProvider) this.Signals.Publish(new FullNodeEvent() { Message = $"Full node initialized on {this.Network.Name}.", State = this.State.ToString() }); this.StartTime = this.DateTimeProvider.GetUtcNow(); + return this; } @@ -201,6 +202,8 @@ public void Start() if (this.State == FullNodeState.Disposing || this.State == FullNodeState.Disposed) throw new ObjectDisposedException(nameof(FullNode)); + this.Signals.Publish(new FullNodeEvent() { Message = $"Full node starting on {this.Network.Name}.", State = this.State.ToString() }); + this.nodeRunningLock = new NodeRunningLock(this.DataFolder); if (!this.nodeRunningLock.TryLockNodeFolder()) @@ -233,6 +236,8 @@ public void Start() this.StartPeriodicLog(); this.State = FullNodeState.Started; + + this.Signals.Publish(new FullNodeEvent() { Message = $"Full node started on {this.Network.Name}.", State = this.State.ToString() }); } /// diff --git a/src/Stratis.Bitcoin/IFullNode.cs b/src/Stratis.Bitcoin/IFullNode.cs index 36315add31..b1ae88e16e 100644 --- a/src/Stratis.Bitcoin/IFullNode.cs +++ b/src/Stratis.Bitcoin/IFullNode.cs @@ -85,6 +85,9 @@ public enum FullNodeState Disposing, /// Assigned when finished executing. - Disposed + Disposed, + + /// Assigned when one of the full node features failed to start. + Failed, } } diff --git a/src/Stratis.Bitcoin/Properties/AssemblyInfo.cs b/src/Stratis.Bitcoin/Properties/AssemblyInfo.cs index eec45629a3..45d57ae75b 100644 --- a/src/Stratis.Bitcoin/Properties/AssemblyInfo.cs +++ b/src/Stratis.Bitcoin/Properties/AssemblyInfo.cs @@ -32,6 +32,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.8.6")] -[assembly: AssemblyFileVersion("1.0.8.6")] +[assembly: AssemblyVersion("1.0.8.7")] +[assembly: AssemblyFileVersion("1.0.8.7")] [assembly: InternalsVisibleTo("Stratis.Bitcoin.Tests")] \ No newline at end of file diff --git a/src/Stratis.Bitcoin/Stratis.Bitcoin.csproj b/src/Stratis.Bitcoin/Stratis.Bitcoin.csproj index 7b8c5de7cb..bc2dbe164c 100644 --- a/src/Stratis.Bitcoin/Stratis.Bitcoin.csproj +++ b/src/Stratis.Bitcoin/Stratis.Bitcoin.csproj @@ -14,7 +14,7 @@ false false false - 1.0.8.6 + 1.0.8.7 False ..\Stratis.ruleset Stratis Group Ltd. diff --git a/src/Stratis.CirrusMinerD/Program.cs b/src/Stratis.CirrusMinerD/Program.cs index 3bf2b95a46..60f2dd4705 100644 --- a/src/Stratis.CirrusMinerD/Program.cs +++ b/src/Stratis.CirrusMinerD/Program.cs @@ -42,7 +42,7 @@ public static async Task MainAsync(string[] args) { bool isMainchainNode = args.FirstOrDefault(a => a.ToLower() == MainchainArgument) != null; bool isSidechainNode = args.FirstOrDefault(a => a.ToLower() == SidechainArgument) != null; - bool startInDevMode = args.FirstOrDefault(a => a.ToLower() == $"-{NodeSettings.DevModeParam}") != null; + bool startInDevMode = args.Any(a => a.ToLower().Contains($"-{NodeSettings.DevModeParam}")); IFullNode fullNode = null; @@ -82,7 +82,7 @@ private static IFullNode BuildCirrusMiningNode(string[] args) .UseBlockStore() .AddPoAFeature() .UsePoAConsensus() - .AddPoACollateralMiningCapability() + .AddPoACollateralMiningCapability() .CheckCollateralCommitment() .AddDynamicMemberhip() .SetCounterChainNetwork(StraxNetwork.MainChainNetworks[nodeSettings.Network.NetworkType]()) diff --git a/src/Stratis.CirrusPegD/Program.cs b/src/Stratis.CirrusPegD/Program.cs index cedd47b2b0..c6924fd478 100644 --- a/src/Stratis.CirrusPegD/Program.cs +++ b/src/Stratis.CirrusPegD/Program.cs @@ -10,6 +10,7 @@ using Stratis.Bitcoin.Features.Api; using Stratis.Bitcoin.Features.BlockStore; using Stratis.Bitcoin.Features.Consensus; +using Stratis.Bitcoin.Features.ExternalApi; using Stratis.Bitcoin.Features.Interop; using Stratis.Bitcoin.Features.MemoryPool; using Stratis.Bitcoin.Features.Miner; @@ -97,6 +98,7 @@ private static IFullNode GetMainchainFullNode(string[] args) .UseWallet() .AddSQLiteWalletRepository() .AddPowPosMining(true) + .AddExternalApi() .Build(); return node; @@ -116,7 +118,7 @@ private static IFullNode GetSidechainFullNode(string[] args) .AddPoAFeature() .UsePoAConsensus() .AddFederatedPeg() - .AddPoACollateralMiningCapability() + .AddPoACollateralMiningCapability() .CheckCollateralCommitment() .AddDynamicMemberhip() .UseTransactionNotification() @@ -132,6 +134,7 @@ private static IFullNode GetSidechainFullNode(string[] args) .AddInteroperability() .UseSmartContractWallet() .AddSQLiteWalletRepository() + .AddExternalApi() .Build(); return node; diff --git a/src/Stratis.CirrusPegD/Stratis.CirrusPegD.csproj b/src/Stratis.CirrusPegD/Stratis.CirrusPegD.csproj index c22f630cf0..48c7f3fa84 100644 --- a/src/Stratis.CirrusPegD/Stratis.CirrusPegD.csproj +++ b/src/Stratis.CirrusPegD/Stratis.CirrusPegD.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Stratis.External.Masternodes/Program.cs b/src/Stratis.External.Masternodes/Program.cs new file mode 100644 index 0000000000..304678fec8 --- /dev/null +++ b/src/Stratis.External.Masternodes/Program.cs @@ -0,0 +1,29 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using NBitcoin; + +namespace Stratis.External.Masternodes +{ + class Program + { + static async Task Main(string[] args) + { + Console.WriteLine("Welcome to the Stratis Masternode Registration application."); + Console.WriteLine("Please press any key to start."); + Console.ReadKey(); + + var service = new RegistrationService(); + + NetworkType networkType = NetworkType.Mainnet; + + if (args.Contains("-testnet")) + networkType = NetworkType.Testnet; + + if (args.Contains("-regtest")) + networkType = NetworkType.Regtest; + + await service.StartAsync(networkType); + } + } +} diff --git a/src/Stratis.External.Masternodes/RegistrationService.cs b/src/Stratis.External.Masternodes/RegistrationService.cs new file mode 100644 index 0000000000..ce7d5d874c --- /dev/null +++ b/src/Stratis.External.Masternodes/RegistrationService.cs @@ -0,0 +1,524 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Flurl; +using Flurl.Http; +using NBitcoin; +using NBitcoin.DataEncoders; +using Stratis.Bitcoin; +using Stratis.Bitcoin.Controllers.Models; +using Stratis.Bitcoin.Features.BlockStore.Models; +using Stratis.Bitcoin.Features.PoA; +using Stratis.Bitcoin.Features.PoA.Models; +using Stratis.Bitcoin.Features.Wallet.Models; +using Stratis.Bitcoin.Networks; +using Stratis.Features.PoA.Voting; +using Stratis.Sidechains.Networks; + +namespace Stratis.External.Masternodes +{ + public sealed class RegistrationService + { + /// The folder where the CirrusMinerD.exe is stored. + private string nodeExecutablesPath; + private Network mainchainNetwork; + private Network sidechainNetwork; + private const string nodeExecutable = "Stratis.CirrusMinerD.exe"; + + private const int CollateralRequirement = 100_000; + private const int FeeRequirement = 500; + + private string rootDataDir; + + public async Task StartAsync(NetworkType networkType) + { + this.rootDataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StratisNode"); + this.nodeExecutablesPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "StraxMinerD"); + + if (networkType == NetworkType.Mainnet) + { + this.mainchainNetwork = new StraxMain(); + this.sidechainNetwork = new CirrusMain(); + } + + if (networkType == NetworkType.Testnet) + { + this.mainchainNetwork = new StraxTest(); + this.sidechainNetwork = new CirrusTest(); + } + + if (networkType == NetworkType.Regtest) + { + this.mainchainNetwork = new StraxRegTest(); + this.sidechainNetwork = new CirrusRegTest(); + } + + // Start main chain node + if (!await StartNodeAsync(networkType, NodeType.MainChain)) + return; + + // Wait for main chain node to be initialized + if (!await EnsureNodeIsInitializedAsync(NodeType.MainChain, this.mainchainNetwork.DefaultAPIPort)) + return; + + // Wait for main chain node to be synced (out of IBD) + if (!await EnsureNodeIsSyncedAsync(NodeType.MainChain, this.mainchainNetwork.DefaultAPIPort)) + return; + + // Wait for main chain node's address indexer to be synced. + if (!await EnsureMainChainNodeAddressIndexerIsSyncedAsync()) + return; + + // Create the masternode public key. + if (!CreateFederationKey()) + return; + + // Start side chain node + if (!await StartNodeAsync(networkType, NodeType.SideChain)) + return; + + // Wait for side chain node to be initialized + if (!await EnsureNodeIsInitializedAsync(NodeType.SideChain, this.sidechainNetwork.DefaultAPIPort)) + return; + + // Wait for side chain node to be synced (out of IBD) + if (!await EnsureNodeIsSyncedAsync(NodeType.SideChain, this.sidechainNetwork.DefaultAPIPort)) + return; + + Console.Clear(); + Console.WriteLine("SUCCESS: STRAX Blockchain and Cirrus Blockchain are now fully synchronised."); + Console.WriteLine("Assessing Masternode Requirements..."); + + // Check main chain collateral wallet and balace + if (!await CheckWalletRequirementsAsync(NodeType.MainChain, this.mainchainNetwork.DefaultAPIPort)) + return; + + // Check side chain fee wallet + if (!await CheckWalletRequirementsAsync(NodeType.SideChain, this.sidechainNetwork.DefaultAPIPort)) + return; + + // Call the join federation API call. + if (!await CallJoinFederationRequestAsync()) + return; + + // Call the join federation API call. + await MonitorJoinFederationRequestAsync(); + } + + private async Task StartNodeAsync(NetworkType networkType, NodeType nodeType) + { + var argumentBuilder = new StringBuilder(); + + argumentBuilder.Append($"-{nodeType.ToString().ToLowerInvariant()} "); + + if (nodeType == NodeType.MainChain) + argumentBuilder.Append("-addressindex=1 "); + + if (nodeType == NodeType.SideChain) + argumentBuilder.Append($"-counterchainapiport={this.mainchainNetwork.DefaultAPIPort} "); + + if (networkType == NetworkType.Testnet) + argumentBuilder.Append("-testnet"); + + if (networkType == NetworkType.Regtest) + argumentBuilder.Append("-regtest"); + + Console.WriteLine($"Starting the {nodeType} node on {networkType}."); + Console.WriteLine($"Start up arguments: {argumentBuilder}"); + + var startInfo = new ProcessStartInfo + { + Arguments = argumentBuilder.ToString(), + FileName = Path.Combine(this.nodeExecutablesPath, nodeExecutable), + UseShellExecute = true, + }; + + var process = Process.Start(startInfo); + await Task.Delay(TimeSpan.FromSeconds(5)); + + if (process.HasExited) + { + Console.WriteLine($"{nodeType} node process failed to start, exiting..."); + return false; + } + + Console.WriteLine($"{nodeType} node started."); + + return true; + } + + private async Task EnsureNodeIsInitializedAsync(NodeType nodeType, int apiPort) + { + Console.WriteLine($"Waiting for the {nodeType} node to initialize..."); + + bool initialized = false; + + // Call the node status API until the node initialization state is Initialized. + CancellationToken cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token; + do + { + if (cancellationTokenSource.IsCancellationRequested) + { + Console.WriteLine($"{nodeType} node failed to initialized in 60 seconds..."); + break; + } + + StatusModel blockModel = await $"http://localhost:{apiPort}/api".AppendPathSegment("node/status").GetJsonAsync(); + if (blockModel.State == FullNodeState.Started.ToString()) + { + initialized = true; + Console.WriteLine($"{nodeType} node initialized."); + break; + } + + } while (true); + + return initialized; + } + + private async Task EnsureNodeIsSyncedAsync(NodeType nodeType, int apiPort) + { + Console.WriteLine($"Waiting for the {nodeType} node to sync with the network..."); + + bool result; + + // Call the node status API until the node initialization state is Initialized. + do + { + StatusModel blockModel = await $"http://localhost:{apiPort}/api".AppendPathSegment("node/status").GetJsonAsync(); + if (blockModel.InIbd.HasValue && !blockModel.InIbd.Value) + { + Console.WriteLine($"{nodeType} node is synced at height {blockModel.ConsensusHeight}."); + result = true; + break; + } + + Console.WriteLine($"{nodeType} node syncing, current height {blockModel.ConsensusHeight}..."); + await Task.Delay(TimeSpan.FromSeconds(3)); + } while (true); + + return result; + } + + private async Task EnsureMainChainNodeAddressIndexerIsSyncedAsync() + { + Console.WriteLine("Waiting for the main chain node to sync it's address indexer..."); + + bool result; + + do + { + StatusModel blockModel = await $"http://localhost:{this.mainchainNetwork.DefaultAPIPort}/api".AppendPathSegment("node/status").GetJsonAsync(); + AddressIndexerTipModel addressIndexerModel = await $"http://localhost:{this.mainchainNetwork.DefaultAPIPort}/api".AppendPathSegment("blockstore/addressindexertip").GetJsonAsync(); + if (addressIndexerModel.TipHeight > (blockModel.ConsensusHeight - 50)) + { + Console.WriteLine($"Main chain address indexer synced."); + result = true; + break; + } + + Console.WriteLine($"Main chain node address indexer is syncing, current height {addressIndexerModel.TipHeight}..."); + await Task.Delay(TimeSpan.FromSeconds(3)); + } while (true); + + return result; + } + + private async Task CheckWalletRequirementsAsync(NodeType nodeType, int apiPort) + { + var chainName = nodeType == NodeType.MainChain ? "STRAX" : "CIRRUS"; + var amountToCheck = nodeType == NodeType.MainChain ? CollateralRequirement : FeeRequirement; + var chainTicker = nodeType == NodeType.MainChain ? this.mainchainNetwork.CoinTicker : this.sidechainNetwork.CoinTicker; + + Console.WriteLine($"Please enter the name of the {chainName} wallet that contains the required collateral of {amountToCheck} {chainTicker}:"); + + var walletName = Console.ReadLine(); + + WalletInfoModel walletInfoModel = await $"http://localhost:{apiPort}/api".AppendPathSegment("Wallet/list-wallets").GetJsonAsync(); + + if (walletInfoModel.WalletNames.Contains(walletName)) + { + Console.WriteLine($"SUCCESS: Wallet with name '{chainName}' found."); + } + else + { + Console.WriteLine($"{chainName} wallet with name '{walletName}' does not exist."); + + ConsoleKeyInfo key; + do + { + Console.WriteLine($"Would you like to restore you {chainName} wallet that holds the required amount of {amountToCheck} {chainTicker} now? Enter (Y) to continue or (N) to exit."); + key = Console.ReadKey(); + if (key.Key == ConsoleKey.Y || key.Key == ConsoleKey.N) + break; + } while (true); + + if (key.Key == ConsoleKey.N) + { + Console.WriteLine($"You have chosen to exit the registration script."); + return false; + } + + if (!await RestoreWalletAsync(apiPort, chainName, walletName)) + return false; + } + + // Check wallet height (sync) status. + do + { + var walletNameRequest = new WalletName() { Name = walletName }; + WalletGeneralInfoModel walletInfo = await $"http://localhost:{apiPort}/api".AppendPathSegment("wallet/general-info").SetQueryParams(walletNameRequest).GetJsonAsync(); + StatusModel blockModel = await $"http://localhost:{apiPort}/api".AppendPathSegment("node/status").GetJsonAsync(); + + if (walletInfo.LastBlockSyncedHeight > (blockModel.ConsensusHeight - 50)) + { + Console.WriteLine($"{chainName} wallet is synced."); + break; + } + + Console.WriteLine($"Syncing {chainName} wallet, current height {walletInfo.LastBlockSyncedHeight}..."); + await Task.Delay(TimeSpan.FromSeconds(3)); + } while (true); + + // Check wallet balance. + try + { + var walletBalanceRequest = new WalletBalanceRequest() { WalletName = walletName }; + WalletBalanceModel walletBalanceModel = await $"http://localhost:{apiPort}/api" + .AppendPathSegment("wallet/balance") + .SetQueryParams(walletBalanceRequest) + .GetJsonAsync(); + + if (walletBalanceModel.AccountsBalances[0].SpendableAmount / 100000000 > amountToCheck) + { + Console.WriteLine($"SUCCESS: The {chainName} wallet contains the required amount of {amountToCheck} {chainTicker}."); + return true; + } + + Console.WriteLine($"ERROR: The {chainName} wallet does not contain the required amount of {amountToCheck} {chainTicker}."); + + } + catch (Exception ex) + { + Console.WriteLine($"ERROR: An exception occurred trying to check the wallet balance: {ex}"); + } + + return false; + } + + private async Task RestoreWalletAsync(int apiPort, string chainName, string walletName) + { + Console.WriteLine($"You have chosen to restore your {chainName} wallet."); + + string mnemonic; + string passphrase; + string password; + + do + { + Console.WriteLine($"Please enter your 12-Words used to recover your wallet:"); + mnemonic = Console.ReadLine(); + Console.WriteLine("Please enter your wallet passphrase:"); + passphrase = Console.ReadLine(); + Console.WriteLine("Please enter the wallet password used to encrypt the wallet:"); + password = Console.ReadLine(); + + if (!string.IsNullOrEmpty(mnemonic) && !string.IsNullOrEmpty(passphrase) && !string.IsNullOrEmpty(password)) + break; + + Console.WriteLine("ERROR: Please ensure that you enter all the wallet details."); + + } while (true); + + var walletRecoveryRequest = new WalletRecoveryRequest() + { + CreationDate = new DateTime(2020, 11, 1), + Mnemonic = mnemonic, + Name = walletName, + Passphrase = passphrase, + Password = password + }; + + try + { + await $"http://localhost:{apiPort}/api".AppendPathSegment("wallet/recover").PostJsonAsync(walletRecoveryRequest); + } + catch (Exception ex) + { + Console.WriteLine($"ERROR: An exception occurred trying to recover your {chainName} wallet: {ex}"); + return false; + } + + WalletInfoModel walletInfoModel = await $"http://localhost:{apiPort}/api".AppendPathSegment("Wallet/list-wallets").GetJsonAsync(); + if (walletInfoModel.WalletNames.Contains(walletName)) + { + Console.WriteLine($"SUCCESS: {chainName} wallet has been restored."); + } + else + { + Console.WriteLine($"ERROR: {chainName} wallet failed to be restored, exiting the registration process."); + return false; + } + + try + { + Console.WriteLine($"Your {chainName} wallet will now be resynced, please be patient..."); + var walletSyncRequest = new WalletSyncRequest() + { + All = true, + WalletName = walletName + }; + + await $"http://localhost:{apiPort}/api".AppendPathSegment("wallet/sync-from-date").PostJsonAsync(walletSyncRequest); + } + catch (Exception ex) + { + Console.WriteLine($"ERROR: An exception occurred trying to resync your {chainName} wallet: {ex}"); + return false; + } + + return true; + } + + private bool CreateFederationKey() + { + var keyFilePath = Path.Combine(this.rootDataDir, this.sidechainNetwork.RootFolderName, this.sidechainNetwork.Name, KeyTool.KeyFileDefaultName); + + if (File.Exists(keyFilePath)) + { + Console.WriteLine($"Your masternode public key file already exists."); + return true; + } + + Console.Clear(); + Console.WriteLine($"Your masternode public key will now be generated."); + + string publicKeyPassphrase; + + do + { + Console.WriteLine($"Please enter a passphrase (this can be anything, but please write it down):"); + publicKeyPassphrase = Console.ReadLine(); + + if (!string.IsNullOrEmpty(publicKeyPassphrase)) + break; + + Console.WriteLine("ERROR: Please ensure that you enter a valid passphrase."); + + } while (true); + + + + // Generate keys for mining. + var tool = new KeyTool(keyFilePath); + + Key key = tool.GeneratePrivateKey(); + + string savePath = tool.GetPrivateKeySavePath(); + tool.SavePrivateKey(key); + PubKey miningPubKey = key.PubKey; + + Console.WriteLine($"Your Masternode Public Key (PubKey) is: {Encoders.Hex.EncodeData(miningPubKey.ToBytes(false))}"); + + if (publicKeyPassphrase != null) + { + Console.WriteLine(Environment.NewLine); + Console.WriteLine($"Your passphrase: {publicKeyPassphrase}"); + } + + Console.WriteLine(Environment.NewLine); + Console.WriteLine($"It has been saved in the root Cirrus data folder: {savePath}"); + Console.WriteLine($"Please ensure that you take a backup of this file."); + return true; + } + + private async Task CallJoinFederationRequestAsync() + { + Console.Clear(); + Console.WriteLine($"The relevant masternode registration wallets has now been setup and verified."); + Console.WriteLine($"Press any key to continue (this will deduct the registation fee from your Cirrus wallet)"); + Console.ReadKey(); + + string collateralWallet; + string collateralPassword; + string collateralAddress; + string cirrusWalletName; + string cirrusWalletPassword; + + do + { + Console.WriteLine($"[Strax] Please enter the collateral wallet name:"); + collateralWallet = Console.ReadLine(); + Console.WriteLine($"[Strax] Please enter the collateral wallet password:"); + collateralPassword = Console.ReadLine(); + Console.WriteLine($"[Strax] Please enter the collateral address in which the collateral amount of {CollateralRequirement} {this.mainchainNetwork.CoinTicker} is held:"); + collateralAddress = Console.ReadLine(); + + Console.WriteLine($"[Cirrus] Please enter the wallet name which holds the registration fee of {FeeRequirement} {this.sidechainNetwork.CoinTicker}:"); + cirrusWalletName = Console.ReadLine(); + Console.WriteLine($"[Cirrus] Please enter the above wallet's password:"); + cirrusWalletPassword = Console.ReadLine(); + + if (!string.IsNullOrEmpty(collateralWallet) && !string.IsNullOrEmpty(collateralPassword) && !string.IsNullOrEmpty(collateralAddress) && + !string.IsNullOrEmpty(cirrusWalletName) && !string.IsNullOrEmpty(cirrusWalletPassword)) + break; + + Console.WriteLine("ERROR: Please ensure that you enter the relevant details correctly."); + + } while (true); + + var request = new JoinFederationRequestModel() + { + CollateralAddress = collateralAddress, + CollateralWalletName = collateralWallet, + CollateralWalletPassword = collateralPassword, + WalletAccount = "account 0", + WalletName = cirrusWalletName, + WalletPassword = cirrusWalletPassword + }; + + try + { + await $"http://localhost:{this.sidechainNetwork.DefaultAPIPort}/api".AppendPathSegment("collateral/joinfederation").PostJsonAsync(request); + Console.WriteLine($"SUCCESS: The masternode request has now been submitted to the network,please press any key to view its progress."); + return true; + } + catch (Exception ex) + { + Console.WriteLine($"ERROR: An exception occurred trying to registre your masternode: {ex}"); + return false; + } + } + + private async Task MonitorJoinFederationRequestAsync() + { + // Check wallet height (sync) status. + do + { + FederationMemberDetailedModel memberInfo = await $"http://localhost:{this.sidechainNetwork.DefaultAPIPort}/api".AppendPathSegment("federation/members/current").GetJsonAsync(); + StatusModel blockModel = await $"http://localhost:{this.sidechainNetwork.DefaultAPIPort}/api".AppendPathSegment("node/status").GetJsonAsync(); + + Console.Clear(); + Console.WriteLine($">> Registration Progress"); + Console.WriteLine($"PubKey".PadRight(30) + $": {memberInfo.PubKey}"); + Console.WriteLine($"Current Height".PadRight(30) + $": {blockModel.ConsensusHeight}"); + Console.WriteLine($"Mining will start at height".PadRight(30) + $": {memberInfo.MemberWillStartMiningAtBlockHeight}"); + Console.WriteLine($"Rewards will start at height".PadRight(30) + $": {memberInfo.MemberWillStartEarningRewardsEstimateHeight}"); + Console.WriteLine(); + Console.WriteLine($"Press CRTL-C to exit..."); + await Task.Delay(TimeSpan.FromSeconds(5)); + } while (true); + } + } + + public enum NodeType + { + MainChain, + SideChain + } +} diff --git a/src/Stratis.External.Masternodes/Stratis.External.Masternodes.csproj b/src/Stratis.External.Masternodes/Stratis.External.Masternodes.csproj new file mode 100644 index 0000000000..47791c4d2a --- /dev/null +++ b/src/Stratis.External.Masternodes/Stratis.External.Masternodes.csproj @@ -0,0 +1,20 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + + + + + + + + diff --git a/src/Stratis.Features.Collateral/CollateralFeature.cs b/src/Stratis.Features.Collateral/CollateralFeature.cs index 408349a5b1..fe819a92ab 100644 --- a/src/Stratis.Features.Collateral/CollateralFeature.cs +++ b/src/Stratis.Features.Collateral/CollateralFeature.cs @@ -5,7 +5,6 @@ using Stratis.Bitcoin.Features.Miner; using Stratis.Bitcoin.Features.PoA; using Stratis.Bitcoin.Features.SmartContracts; -using Stratis.Bitcoin.Features.SmartContracts.PoA; using Stratis.Features.Collateral.CounterChain; namespace Stratis.Features.Collateral @@ -49,7 +48,7 @@ public static IFullNodeBuilder CheckCollateralCommitment(this IFullNodeBuilder f /// /// Adds mining to the side chain node when on a proof-of-authority network with collateral enabled. /// - public static IFullNodeBuilder AddPoACollateralMiningCapability(this IFullNodeBuilder fullNodeBuilder) + public static IFullNodeBuilder AddPoACollateralMiningCapability(this IFullNodeBuilder fullNodeBuilder) where T : BlockDefinition { // Inject the CheckCollateralFullValidationRule as the first Full Validation Rule. // This is still a bit hacky and we need to properly review the dependencies again between the different side chain nodes. @@ -65,7 +64,7 @@ public static IFullNodeBuilder AddPoACollateralMiningCapability(this IFullNodeBu { services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Stratis.Features.FederatedPeg.IntegrationTests/Utils/MainChainFederationNodeRunner.cs b/src/Stratis.Features.FederatedPeg.IntegrationTests/Utils/MainChainFederationNodeRunner.cs index 3dd697d071..2ab0869414 100644 --- a/src/Stratis.Features.FederatedPeg.IntegrationTests/Utils/MainChainFederationNodeRunner.cs +++ b/src/Stratis.Features.FederatedPeg.IntegrationTests/Utils/MainChainFederationNodeRunner.cs @@ -49,6 +49,7 @@ public override void BuildNode() .UsePosConsensus() .UseWallet() .AddSQLiteWalletRepository() + // TODO: Change to true and fix tests .AddPowPosMining(false) .MockIBD() .Build(); diff --git a/src/Stratis.Features.FederatedPeg.IntegrationTests/Utils/SidechainFederationNodeRunner.cs b/src/Stratis.Features.FederatedPeg.IntegrationTests/Utils/SidechainFederationNodeRunner.cs index a597dd2e3a..6839c82e63 100644 --- a/src/Stratis.Features.FederatedPeg.IntegrationTests/Utils/SidechainFederationNodeRunner.cs +++ b/src/Stratis.Features.FederatedPeg.IntegrationTests/Utils/SidechainFederationNodeRunner.cs @@ -52,7 +52,7 @@ public override void BuildNode() .AddPoAFeature() .UsePoAConsensus() .AddFederatedPeg() - .AddPoACollateralMiningCapability() + .AddPoACollateralMiningCapability() .CheckCollateralCommitment() .UseTransactionNotification() .UseBlockNotification() diff --git a/src/Stratis.Features.FederatedPeg.IntegrationTests/Utils/SidechainMinerNodeRunner.cs b/src/Stratis.Features.FederatedPeg.IntegrationTests/Utils/SidechainMinerNodeRunner.cs index d4cfb40353..d5ad37d5b3 100644 --- a/src/Stratis.Features.FederatedPeg.IntegrationTests/Utils/SidechainMinerNodeRunner.cs +++ b/src/Stratis.Features.FederatedPeg.IntegrationTests/Utils/SidechainMinerNodeRunner.cs @@ -46,7 +46,7 @@ public override void BuildNode() .SetCounterChainNetwork(this.counterChainNetwork) .AddPoAFeature() .UsePoAConsensus() - .AddPoACollateralMiningCapability() + .AddPoACollateralMiningCapability() .AddDynamicMemberhip() .UseTransactionNotification() .UseBlockNotification() diff --git a/src/Stratis.Features.FederatedPeg.Tests/ControllersTests/FederationGatewayControllerTests.cs b/src/Stratis.Features.FederatedPeg.Tests/ControllersTests/FederationGatewayControllerTests.cs index 6d4084b394..d074a678ae 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/ControllersTests/FederationGatewayControllerTests.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/ControllersTests/FederationGatewayControllerTests.cs @@ -15,6 +15,7 @@ using Stratis.Bitcoin.Connection; using Stratis.Bitcoin.Consensus; using Stratis.Bitcoin.Features.BlockStore; +using Stratis.Bitcoin.Features.ExternalApi; using Stratis.Bitcoin.Features.PoA; using Stratis.Bitcoin.Features.PoA.Voting; using Stratis.Bitcoin.Networks; @@ -110,7 +111,9 @@ private MaturedBlocksProvider GetMaturedBlocksProvider(IFederatedPegSettings fed return blocks; }); - return new MaturedBlocksProvider(this.consensusManager, this.depositExtractor, federatedPegSettings); + IExternalApiPoller externalApiPoller = Substitute.For(); + + return new MaturedBlocksProvider(this.consensusManager, this.depositExtractor, federatedPegSettings, externalApiPoller); } [Fact] diff --git a/src/Stratis.Features.FederatedPeg.Tests/ControllersTests/FederationWalletControllerTests.cs b/src/Stratis.Features.FederatedPeg.Tests/ControllersTests/FederationWalletControllerTests.cs index 0c3d4a9480..33ab1c45e2 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/ControllersTests/FederationWalletControllerTests.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/ControllersTests/FederationWalletControllerTests.cs @@ -81,7 +81,7 @@ public void GetGeneralInfo() Assert.Equal(this.fedWallet.CreationTime, model.CreationTime); Assert.Equal(this.fedWallet.LastBlockSyncedHeight, model.LastBlockSyncedHeight); - Assert.Equal(this.fedWallet.Network, model.Network); + Assert.Equal(this.fedWallet.Network.Name, model.Network); } [Fact] diff --git a/src/Stratis.Features.FederatedPeg.Tests/CrossChainTransferStoreTests.cs b/src/Stratis.Features.FederatedPeg.Tests/CrossChainTransferStoreTests.cs index 601983acd7..33de90b742 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/CrossChainTransferStoreTests.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/CrossChainTransferStoreTests.cs @@ -10,6 +10,7 @@ using Stratis.Bitcoin.Configuration; using Stratis.Bitcoin.Consensus; using Stratis.Bitcoin.Controllers; +using Stratis.Bitcoin.Features.ExternalApi; using Stratis.Bitcoin.Features.MemoryPool; using Stratis.Bitcoin.Features.Wallet.Models; using Stratis.Bitcoin.Tests.Common; @@ -604,7 +605,8 @@ public void DoTest() var transaction = new PosTransaction(model.Hex); var reader = new OpReturnDataReader(new CounterChainNetworkWrapper(CirrusNetwork.NetworksSelector.Testnet())); - var extractor = new DepositExtractor(this.federatedPegSettings, this.network, reader); + IExternalApiPoller externalApiPoller = Substitute.For(); + var extractor = new DepositExtractor(this.federatedPegSettings, this.network, reader, externalApiPoller); IDeposit deposit = extractor.ExtractDepositFromTransaction(transaction, 2, 1); Assert.NotNull(deposit); diff --git a/src/Stratis.Features.FederatedPeg.Tests/DepositExtractorTests.cs b/src/Stratis.Features.FederatedPeg.Tests/DepositExtractorTests.cs index 6eaf36adfb..1d21cefcfa 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/DepositExtractorTests.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/DepositExtractorTests.cs @@ -4,6 +4,7 @@ using FluentAssertions; using NBitcoin; using NSubstitute; +using Stratis.Bitcoin.Features.ExternalApi; using Stratis.Bitcoin.Networks; using Stratis.Features.FederatedPeg.Interfaces; using Stratis.Features.FederatedPeg.SourceChain; @@ -38,7 +39,8 @@ public DepositExtractorTests() this.opReturnDataReader = Substitute.For(); this.opReturnDataReader.TryGetTargetAddress(null, out string address).Returns(callInfo => { callInfo[1] = null; return false; }); - this.depositExtractor = new DepositExtractor(this.federationSettings, this.network, this.opReturnDataReader); + IExternalApiPoller externalApiPoller = Substitute.For(); + this.depositExtractor = new DepositExtractor(this.federationSettings, this.network, this.opReturnDataReader, externalApiPoller); this.transactionBuilder = new TestTransactionBuilder(); } @@ -152,6 +154,34 @@ public void ExtractNormalDeposits_ReturnDeposits_AboveFasterThreshold() } } + [Fact] + public void ExtractNormalConversionDeposits_ReturnDeposits_AboveFasterThreshold() + { + Block block = this.network.Consensus.ConsensusFactory.CreateBlock(); + + BitcoinPubKeyAddress targetAddress = this.addressHelper.GetNewTargetChainPubKeyAddress(); + byte[] opReturnBytes = Encoding.UTF8.GetBytes(targetAddress.ToString()); + + // Set amount to be less than the small threshold amount. + CreateDepositTransaction(targetAddress, block, this.federationSettings.SmallDepositThresholdAmount - 1, opReturnBytes); + + // Set amount to be exactly the small threshold amount. + CreateConversionTransaction(TargetEthereumAddress, block, this.federationSettings.SmallDepositThresholdAmount, opReturnBytes); + + // Set amount to be greater than the small threshold amount. + CreateConversionTransaction(TargetEthereumAddress, block, this.federationSettings.SmallDepositThresholdAmount + 1, opReturnBytes); + + int blockHeight = 12345; + IReadOnlyList extractedDeposits = this.depositExtractor.ExtractDepositsFromBlock(block, blockHeight, new[] { DepositRetrievalType.ConversionNormal }); + + // Should only be 1, with the value just over the withdrawal fee. + extractedDeposits.Count.Should().Be(1); + foreach (IDeposit extractedDeposit in extractedDeposits) + { + Assert.True(extractedDeposit.Amount >= this.federationSettings.SmallDepositThresholdAmount); + } + } + // Normal Deposits [Fact] public void ExtractNormalDeposits_ReturnDeposits_AboveSmallThreshold_BelowEqualToNormalThreshold() @@ -223,6 +253,41 @@ public void ExtractSmallDeposits_ReturnDeposits_BelowSmallThreshold_AboveMinimum } } + [Fact] + public void ExtractSmallConversionDeposits_ReturnDeposits_BelowSmallThreshold_AboveMinimum() + { + Block block = this.network.Consensus.ConsensusFactory.CreateBlock(); + + // Create the target address. + BitcoinPubKeyAddress targetAddress = this.addressHelper.GetNewTargetChainPubKeyAddress(); + byte[] opReturnBytes = Encoding.UTF8.GetBytes(targetAddress.ToString()); + + // Set amount to be less than deposit minimum + CreateConversionTransaction(TargetEthereumAddress, block, FederatedPegSettings.CrossChainTransferMinimum - 1, opReturnBytes); + + // Set amount to be less than the small threshold amount. + CreateConversionTransaction(TargetEthereumAddress, block, this.federationSettings.SmallDepositThresholdAmount - 1, opReturnBytes); + + // Set amount to be exactly the small threshold amount. + CreateConversionTransaction(TargetEthereumAddress, block, this.federationSettings.SmallDepositThresholdAmount, opReturnBytes); + + // Set amount to be greater than the small threshold amount. + CreateConversionTransaction(TargetEthereumAddress, block, this.federationSettings.SmallDepositThresholdAmount + 1, opReturnBytes); + + // Set amount to be greater than the normal threshold amount. + CreateConversionTransaction(TargetEthereumAddress, block, this.federationSettings.NormalDepositThresholdAmount + 1, opReturnBytes); + + int blockHeight = 12345; + IReadOnlyList extractedDeposits = this.depositExtractor.ExtractDepositsFromBlock(block, blockHeight, new[] { DepositRetrievalType.ConversionSmall }); + + // Should only be two, with the value just over the withdrawal fee. + extractedDeposits.Count.Should().Be(2); + foreach (IDeposit extractedDeposit in extractedDeposits) + { + Assert.True(extractedDeposit.Amount <= this.federationSettings.SmallDepositThresholdAmount); + } + } + // Large Deposits [Fact] public void ExtractLargeDeposits_ReturnDeposits_AboveNormalThreshold() @@ -281,8 +346,8 @@ public void ExtractLargeConversionDeposits_ReturnDeposits_AboveNormalThreshold() // Set amount to be equal to the normal threshold amount. CreateConversionTransaction(TargetEthereumAddress, block, this.federationSettings.NormalDepositThresholdAmount, opReturnBytes); - // Set amount to be greater than the conversion deposit minimum amount. - CreateConversionTransaction(TargetEthereumAddress, block, Money.Coins(DepositExtractor.ConversionTransactionMinimum + 1), opReturnBytes); + // Set amount to be greater than the normal threshold amount. + CreateConversionTransaction(TargetEthereumAddress, block, this.federationSettings.NormalDepositThresholdAmount + 1, opReturnBytes); int blockHeight = 12345; IReadOnlyList extractedDeposits = this.depositExtractor.ExtractDepositsFromBlock(block, blockHeight, new[] { DepositRetrievalType.ConversionLarge }); diff --git a/src/Stratis.Features.FederatedPeg.Tests/Distribution/RewardClaimerTests.cs b/src/Stratis.Features.FederatedPeg.Tests/Distribution/RewardClaimerTests.cs index 4d6bcaf4f6..39f6176d49 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/Distribution/RewardClaimerTests.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/Distribution/RewardClaimerTests.cs @@ -4,6 +4,7 @@ using NSubstitute; using Stratis.Bitcoin.Configuration; using Stratis.Bitcoin.Consensus; +using Stratis.Bitcoin.Features.ExternalApi; using Stratis.Bitcoin.Features.Wallet.Interfaces; using Stratis.Bitcoin.Interfaces; using Stratis.Bitcoin.Networks; @@ -86,7 +87,8 @@ public void RewardClaimer_RetrieveSingleDeposits() this.blocks = ChainedHeadersHelper.CreateConsecutiveHeadersAndBlocks(30, true, network: this.network, chainIndexer: this.chainIndexer, withCoinbaseAndCoinStake: true, createCirrusReward: true); using (var rewardClaimer = new RewardClaimer(this.broadCasterManager, this.chainIndexer, this.consensusManager, this.initialBlockDownloadState, keyValueRepository, this.network, this.signals)) { - var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader); + IExternalApiPoller externalApiPoller = Substitute.For(); + var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader, externalApiPoller); // Add 5 distribution deposits from block 11 through to 15. for (int i = 11; i <= 15; i++) @@ -122,7 +124,8 @@ public void RewardClaimer_RetrieveBatchedDeposits() Assert.Equal(2, rewardTransaction.Outputs.Count); Assert.Equal(Money.Coins(90), rewardTransaction.TotalOut); - var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader); + IExternalApiPoller externalApiPoller = Substitute.For(); + var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader, externalApiPoller); IDeposit deposit = depositExtractor.ExtractDepositFromTransaction(rewardTransaction, 30, this.blocks[30].Block.GetHash()); Assert.Equal(Money.Coins(90), deposit.Amount); } diff --git a/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksProviderTests.cs b/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksProviderTests.cs index 010f54a840..6a5f0c9d05 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksProviderTests.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksProviderTests.cs @@ -7,6 +7,7 @@ using NSubstitute; using NSubstitute.Core; using Stratis.Bitcoin.Consensus; +using Stratis.Bitcoin.Features.ExternalApi; using Stratis.Bitcoin.Networks; using Stratis.Bitcoin.Primitives; using Stratis.Bitcoin.Tests.Common; @@ -91,7 +92,8 @@ public void GetMaturedBlocksReturnsDeposits() this.consensusManager.Tip.Returns(tip); // Makes every block a matured block. - var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, federatedPegSettings); + IExternalApiPoller externalApiPoller = Substitute.For(); + var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, federatedPegSettings, externalApiPoller); SerializableResult> depositsResult = maturedBlocksProvider.RetrieveDeposits(0); @@ -137,9 +139,9 @@ public void RetrieveDeposits_ReturnsDataToAdvanceNextMaturedBlockHeight() return hashes.Select((hash) => this.blocks.Single(x => x.ChainedHeader.HashBlock == hash && x.ChainedHeader.Height <= this.consensusManager.Tip.Height)).ToArray(); }); - var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader); - - var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, this.federatedPegSettings); + IExternalApiPoller externalApiPoller = Substitute.For(); + var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader, externalApiPoller); + var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, this.federatedPegSettings, externalApiPoller); int nextMaturedBlockHeight = 1; for (int i = 1; i < this.blocks.Count; i++) @@ -198,9 +200,9 @@ public void RetrieveDeposits_ReturnsSmallAndNormalDeposits_Scenario2() }); this.consensusManager.Tip.Returns(this.blocks.Last().ChainedHeader); - var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader); - - var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, this.federatedPegSettings); + IExternalApiPoller externalApiPoller = Substitute.For(); + var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader, externalApiPoller); + var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, this.federatedPegSettings, externalApiPoller); SerializableResult> depositsResult = maturedBlocksProvider.RetrieveDeposits(5); @@ -251,10 +253,10 @@ public void RetrieveDeposits_ReturnsSmallAndNormalDeposits_Scenario3() } this.consensusManager.Tip.Returns(this.blocks.Last().ChainedHeader); + IExternalApiPoller externalApiPoller = Substitute.For(); + var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader, externalApiPoller); - var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader); - - var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, this.federatedPegSettings); + var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, this.federatedPegSettings, externalApiPoller); SerializableResult> depositsResult = maturedBlocksProvider.RetrieveDeposits(5); @@ -308,9 +310,9 @@ public void RetrieveDeposits_ReturnsSmallAndNormalDeposits_Scenario4() }); this.consensusManager.Tip.Returns(this.blocks.Last().ChainedHeader); - var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader); - - var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, this.federatedPegSettings); + IExternalApiPoller externalApiPoller = Substitute.For(); + var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader, externalApiPoller); + var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, this.federatedPegSettings, externalApiPoller); SerializableResult> depositsResult = maturedBlocksProvider.RetrieveDeposits(10); @@ -365,9 +367,9 @@ public void RetrieveDeposits_ReturnsSmallAndNormalDeposits_Scenario5() }); this.consensusManager.Tip.Returns(this.blocks.Last().ChainedHeader); - var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader); - - var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, this.federatedPegSettings); + IExternalApiPoller externalApiPoller = Substitute.For(); + var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader, externalApiPoller); + var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, this.federatedPegSettings, externalApiPoller); SerializableResult> depositsResult = maturedBlocksProvider.RetrieveDeposits(10); @@ -421,10 +423,9 @@ public void RetrieveDeposits_ReturnsLargeDeposits_Scenario6() } this.consensusManager.Tip.Returns(this.blocks.Last().ChainedHeader); - - var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader); - - var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, this.federatedPegSettings); + IExternalApiPoller externalApiPoller = Substitute.For(); + var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader, externalApiPoller); + var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, this.federatedPegSettings, externalApiPoller); SerializableResult> depositsResult = maturedBlocksProvider.RetrieveDeposits(10); @@ -484,9 +485,9 @@ public void RetrieveDeposits_ReturnsLargeDeposits_Scenario7() }); this.consensusManager.Tip.Returns(this.blocks.Last().ChainedHeader); - var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader); - - var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, this.federatedPegSettings); + IExternalApiPoller externalApiPoller = Substitute.For(); + var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader, externalApiPoller); + var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, this.federatedPegSettings, externalApiPoller); SerializableResult> depositsResult = maturedBlocksProvider.RetrieveDeposits(10); @@ -547,9 +548,9 @@ public void RetrieveDeposits_ReturnsLargeDeposits_Scenario8() }); this.consensusManager.Tip.Returns(this.blocks.Last().ChainedHeader); - var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader); - - var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, this.federatedPegSettings); + IExternalApiPoller externalApiPoller = Substitute.For(); + var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader, externalApiPoller); + var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, this.federatedPegSettings, externalApiPoller); SerializableResult> depositsResult = maturedBlocksProvider.RetrieveDeposits(10); diff --git a/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksSyncManagerTests.cs b/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksSyncManagerTests.cs index d531efb42e..6ff4eeb6cd 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksSyncManagerTests.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksSyncManagerTests.cs @@ -61,7 +61,7 @@ public async Task BlocksAreRequestedIfThereIsSomethingToRequestAsync() private class TestOnlyMaturedBlocksSyncManager : MaturedBlocksSyncManager { public TestOnlyMaturedBlocksSyncManager(IAsyncProvider asyncProvider, ICrossChainTransferStore crossChainTransferStore, IFederationGatewayClient federationGatewayClient, INodeLifetime nodeLifetime) - : base(asyncProvider, crossChainTransferStore, federationGatewayClient, nodeLifetime, null, null) + : base(asyncProvider, crossChainTransferStore, federationGatewayClient, nodeLifetime, null, null, null, null) { } diff --git a/src/Stratis.Features.FederatedPeg/Controllers/FederationWalletController.cs b/src/Stratis.Features.FederatedPeg/Controllers/FederationWalletController.cs index ec0c7ee36c..7b3eae1754 100644 --- a/src/Stratis.Features.FederatedPeg/Controllers/FederationWalletController.cs +++ b/src/Stratis.Features.FederatedPeg/Controllers/FederationWalletController.cs @@ -88,7 +88,7 @@ public IActionResult GetGeneralInfo() var model = new WalletGeneralInfoModel { - Network = wallet.Network, + Network = wallet.Network.Name, CreationTime = wallet.CreationTime, LastBlockSyncedHeight = wallet.LastBlockSyncedHeight, ConnectedNodes = this.connectionManager.ConnectedPeers.Count(), diff --git a/src/Stratis.Features.FederatedPeg/Distribution/IRewardDistributionManager.cs b/src/Stratis.Features.FederatedPeg/Distribution/IRewardDistributionManager.cs index f70a71a33d..ed37380b09 100644 --- a/src/Stratis.Features.FederatedPeg/Distribution/IRewardDistributionManager.cs +++ b/src/Stratis.Features.FederatedPeg/Distribution/IRewardDistributionManager.cs @@ -6,6 +6,8 @@ namespace Stratis.Features.FederatedPeg.Distribution { public interface IRewardDistributionManager { + List DistributeToMultisigNodes(int blockHeight, Money totalReward); + /// /// Finds the proportion of blocks mined by each miner. /// Creates a corresponding list of recipient scriptPubKeys and reward amounts. diff --git a/src/Stratis.Features.FederatedPeg/Distribution/RewardDistributionManager.cs b/src/Stratis.Features.FederatedPeg/Distribution/RewardDistributionManager.cs index 0feec9d10a..ab1fa4d03f 100644 --- a/src/Stratis.Features.FederatedPeg/Distribution/RewardDistributionManager.cs +++ b/src/Stratis.Features.FederatedPeg/Distribution/RewardDistributionManager.cs @@ -4,6 +4,7 @@ using NBitcoin; using NLog; using Stratis.Bitcoin.Consensus; +using Stratis.Bitcoin.Features.PoA; using Stratis.Features.FederatedPeg.Wallet; using Stratis.Features.PoA.Collateral; @@ -24,6 +25,7 @@ public sealed class RewardDistributionManager : IRewardDistributionManager private readonly int epochWindow; private readonly ILogger logger; private readonly Network network; + private readonly IFederationHistory federationHistory; private readonly Dictionary blocksMinedEach = new Dictionary(); private readonly Dictionary commitmentTransactionByHashDictionary = new Dictionary(); @@ -35,12 +37,13 @@ public sealed class RewardDistributionManager : IRewardDistributionManager // We pay no attention to whether a miner has been kicked since the last distribution or not. // If they produced an accepted block, they get their reward. - public RewardDistributionManager(Network network, ChainIndexer chainIndexer, IConsensusManager consensusManager) + public RewardDistributionManager(Network network, ChainIndexer chainIndexer, IConsensusManager consensusManager, IFederationHistory federationHistory) { this.network = network; this.chainIndexer = chainIndexer; this.consensusManager = consensusManager; this.logger = LogManager.GetCurrentClassLogger(); + this.federationHistory = federationHistory; this.encoder = new CollateralHeightCommitmentEncoder(); this.epoch = this.network.Consensus.MaxReorgLength == 0 ? DefaultEpoch : (int)this.network.Consensus.MaxReorgLength; @@ -57,6 +60,37 @@ public RewardDistributionManager(Network network, ChainIndexer chainIndexer, ICo } } + public List DistributeToMultisigNodes(int blockHeight, Money totalReward) + { + List federation = this.federationHistory.GetFederationForBlock(this.chainIndexer.GetHeader(blockHeight)); + + var multisigs = new List(); + + foreach (IFederationMember member in federation) + { + if (!(member is CollateralFederationMember collateralMember)) + continue; + + if (!collateralMember.IsMultisigMember) + continue; + + multisigs.Add(collateralMember); + } + + Money reward = totalReward / multisigs.Count; + + var recipients = new List(); + + foreach (CollateralFederationMember multisig in multisigs) + { + Script p2pkh = PayToPubkeyHashTemplate.Instance.GenerateScriptPubKey(multisig.PubKey); + + recipients.Add(new Recipient() { Amount = reward, ScriptPubKey = p2pkh }); + } + + return recipients; + } + /// public List Distribute(int heightOfRecordedDistributionDeposit, Money totalReward) { diff --git a/src/Stratis.Features.FederatedPeg/FederatedPegFeature.cs b/src/Stratis.Features.FederatedPeg/FederatedPegFeature.cs index 232f15b413..0b184483e2 100644 --- a/src/Stratis.Features.FederatedPeg/FederatedPegFeature.cs +++ b/src/Stratis.Features.FederatedPeg/FederatedPegFeature.cs @@ -13,7 +13,6 @@ using Stratis.Bitcoin.Configuration; using Stratis.Bitcoin.Configuration.Logging; using Stratis.Bitcoin.Connection; -using Stratis.Bitcoin.Features.Miner; using Stratis.Bitcoin.Features.Notifications; using Stratis.Bitcoin.Features.SmartContracts; using Stratis.Bitcoin.P2P.Peer; @@ -227,6 +226,9 @@ private string CollectStats() var target = d.deposit.TargetAddress; if (target == this.network.CirrusRewardDummyAddress) target = "Reward Distribution"; + if (target == this.network.ConversionTransactionFeeDistributionDummyAddress) + target = "Conversion Fee"; + return $"{d.deposit.Amount} ({d.blocksBeforeMature}) => {target} ({d.deposit.RetrievalType})"; }).Take(10))); @@ -289,7 +291,6 @@ public static IFullNodeBuilder AddFederatedPeg(this IFullNodeBuilder fullNodeBui { services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); } diff --git a/src/Stratis.Features.FederatedPeg/SourceChain/DepositExtractor.cs b/src/Stratis.Features.FederatedPeg/SourceChain/DepositExtractor.cs index d702e478bd..133af5753b 100644 --- a/src/Stratis.Features.FederatedPeg/SourceChain/DepositExtractor.cs +++ b/src/Stratis.Features.FederatedPeg/SourceChain/DepositExtractor.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using System.Linq; using NBitcoin; +using NLog; +using Stratis.Bitcoin.Features.ExternalApi; using Stratis.Features.FederatedPeg.Interfaces; namespace Stratis.Features.FederatedPeg.SourceChain @@ -23,13 +25,17 @@ public sealed class DepositExtractor : IDepositExtractor private readonly IFederatedPegSettings federatedPegSettings; private readonly Network network; private readonly IOpReturnDataReader opReturnDataReader; + private readonly IExternalApiPoller externalApiPoller; + private readonly ILogger logger; - public DepositExtractor(IFederatedPegSettings federatedPegSettings, Network network, IOpReturnDataReader opReturnDataReader) + public DepositExtractor(IFederatedPegSettings federatedPegSettings, Network network, IOpReturnDataReader opReturnDataReader, IExternalApiPoller externalApiPoller) { this.depositScript = federatedPegSettings.MultiSigRedeemScript.PaymentScript; this.federatedPegSettings = federatedPegSettings; this.network = network; this.opReturnDataReader = opReturnDataReader; + this.externalApiPoller = externalApiPoller; + this.logger = LogManager.GetCurrentClassLogger(); } /// @@ -93,8 +99,17 @@ public IDeposit ExtractDepositFromTransaction(Transaction transaction, int block if (conversionTransaction) { - if (amount < Money.Coins(ConversionTransactionMinimum)) + // Instead of a fixed minimum, check that the deposit size at least covers the fee. + decimal minimumDeposit = this.externalApiPoller.EstimateConversionTransactionFee(); + + if (amount < Money.Coins(minimumDeposit)) + { + this.logger.Warn("Received deposit of {0}, but computed minimum deposit fee is {1}. Ignoring deposit.", amount, minimumDeposit); + return null; + } + + this.logger.Info("Received conversion transaction deposit of {0}, subtracting estimated fee of {1}.", amount, minimumDeposit); if (amount > this.federatedPegSettings.NormalDepositThresholdAmount) depositRetrievalType = DepositRetrievalType.ConversionLarge; diff --git a/src/Stratis.Features.FederatedPeg/SourceChain/MaturedBlocksProvider.cs b/src/Stratis.Features.FederatedPeg/SourceChain/MaturedBlocksProvider.cs index b487c2b07c..3ead49238d 100644 --- a/src/Stratis.Features.FederatedPeg/SourceChain/MaturedBlocksProvider.cs +++ b/src/Stratis.Features.FederatedPeg/SourceChain/MaturedBlocksProvider.cs @@ -7,6 +7,7 @@ using NLog; using Stratis.Bitcoin.Consensus; using Stratis.Bitcoin.Controllers; +using Stratis.Bitcoin.Features.ExternalApi; using Stratis.Bitcoin.Primitives; using Stratis.Bitcoin.Utilities; using Stratis.Features.FederatedPeg.Interfaces; @@ -50,8 +51,9 @@ public sealed class MaturedBlocksProvider : IMaturedBlocksProvider private readonly IFederatedPegSettings federatedPegSettings; private readonly ILogger logger; private readonly Dictionary retrievalTypeConfirmations; + private readonly IExternalApiPoller externalApiPoller; - public MaturedBlocksProvider(IConsensusManager consensusManager, IDepositExtractor depositExtractor, IFederatedPegSettings federatedPegSettings) + public MaturedBlocksProvider(IConsensusManager consensusManager, IDepositExtractor depositExtractor, IFederatedPegSettings federatedPegSettings, IExternalApiPoller externalApiPoller) { this.consensusManager = consensusManager; this.depositExtractor = depositExtractor; @@ -74,6 +76,8 @@ public MaturedBlocksProvider(IConsensusManager consensusManager, IDepositExtract this.retrievalTypeConfirmations[DepositRetrievalType.ConversionNormal] = this.federatedPegSettings.MinimumConfirmationsNormalDeposits; this.retrievalTypeConfirmations[DepositRetrievalType.ConversionLarge] = this.federatedPegSettings.MinimumConfirmationsLargeDeposits; } + + this.externalApiPoller = externalApiPoller; } /// @@ -82,6 +86,12 @@ public SerializableResult> RetrieveDeposits(int if (this.consensusManager.Tip == null) return SerializableResult>.Fail("Consensus is not ready to provide blocks (it is un-initialized or still starting up)."); + int gasPrice = this.externalApiPoller.GetGasPrice(); + decimal stratisPrice = this.externalApiPoller.GetStratisPrice(); + + if (gasPrice == -1 || stratisPrice == -1) + return SerializableResult>.Fail("Pricing data not yet available from external API pollers."); + var result = new SerializableResult> { Value = new List(), diff --git a/src/Stratis.Features.FederatedPeg/Stratis.Features.FederatedPeg.csproj b/src/Stratis.Features.FederatedPeg/Stratis.Features.FederatedPeg.csproj index 0533d830c7..7cdb200ab9 100644 --- a/src/Stratis.Features.FederatedPeg/Stratis.Features.FederatedPeg.csproj +++ b/src/Stratis.Features.FederatedPeg/Stratis.Features.FederatedPeg.csproj @@ -18,6 +18,7 @@ + diff --git a/src/Stratis.Features.FederatedPeg/TargetChain/MaturedBlocksSyncManager.cs b/src/Stratis.Features.FederatedPeg/TargetChain/MaturedBlocksSyncManager.cs index c4f168df14..4bbff17a1a 100644 --- a/src/Stratis.Features.FederatedPeg/TargetChain/MaturedBlocksSyncManager.cs +++ b/src/Stratis.Features.FederatedPeg/TargetChain/MaturedBlocksSyncManager.cs @@ -5,6 +5,7 @@ using NBitcoin; using NLog; using Stratis.Bitcoin.AsyncWork; +using Stratis.Bitcoin.Features.ExternalApi; using Stratis.Bitcoin.Utilities; using Stratis.Features.FederatedPeg.Controllers; using Stratis.Features.FederatedPeg.Conversion; @@ -35,6 +36,8 @@ public class MaturedBlocksSyncManager : IMaturedBlocksSyncManager private readonly INodeLifetime nodeLifetime; private readonly IConversionRequestRepository conversionRequestRepository; private readonly ChainIndexer chainIndexer; + private readonly IExternalApiPoller externalApiPoller; + private readonly Network network; private IAsyncLoop requestDepositsTask; @@ -46,7 +49,7 @@ public class MaturedBlocksSyncManager : IMaturedBlocksSyncManager private const int InitializationDelaySeconds = 10; public MaturedBlocksSyncManager(IAsyncProvider asyncProvider, ICrossChainTransferStore crossChainTransferStore, IFederationGatewayClient federationGatewayClient, - INodeLifetime nodeLifetime, IConversionRequestRepository conversionRequestRepository, ChainIndexer chainIndexer) + INodeLifetime nodeLifetime, IConversionRequestRepository conversionRequestRepository, ChainIndexer chainIndexer, IExternalApiPoller externalApiPoller, Network network) { this.asyncProvider = asyncProvider; this.crossChainTransferStore = crossChainTransferStore; @@ -54,6 +57,8 @@ public MaturedBlocksSyncManager(IAsyncProvider asyncProvider, ICrossChainTransfe this.nodeLifetime = nodeLifetime; this.conversionRequestRepository = conversionRequestRepository; this.chainIndexer = chainIndexer; + this.externalApiPoller = externalApiPoller; + this.network = network; this.logger = LogManager.GetCurrentClassLogger(); } @@ -122,11 +127,11 @@ private async Task ProcessMatureBlockDepositsAsync(SerializableResult ProcessMatureBlockDepositsAsync(SerializableResult= conversionTransaction.Amount) + { + this.logger.Warn("Conversion transaction {0} is no longer large enough to cover the fee.", conversionTransaction.Id); + + continue; + } + + // We insert the fee distribution as a deposit to be processed, albeit with a special address. + // Deposits with this address as their destination will be distributed between the multisig members. + var tempList = new List + { + new Deposit(conversionTransaction.Id, conversionTransaction.RetrievalType, Money.Coins(conversionFeeAmount), this.network.ConversionTransactionFeeDistributionDummyAddress, conversionTransaction.BlockNumber, conversionTransaction.BlockHash) + }; + + tempList.AddRange(maturedBlockDeposit.Deposits); + + maturedBlockDeposit.Deposits = tempList.AsReadOnly(); + this.conversionRequestRepository.Save(new ConversionRequest() { RequestId = conversionTransaction.Id.ToString(), RequestType = (int)ConversionRequestType.Mint, Processed = false, RequestStatus = (int)ConversionRequestStatus.Unprocessed, // We do NOT convert to wei here yet. That is done when the minting transaction is submitted on the Ethereum network. - Amount = (ulong)conversionTransaction.Amount.Satoshi, + Amount = (ulong)(conversionTransaction.Amount - Money.Coins(conversionFeeAmount)).Satoshi, BlockHeight = header.Height, DestinationAddress = conversionTransaction.TargetAddress }); diff --git a/src/Stratis.Features.FederatedPeg/TargetChain/WithdrawalTransactionBuilder.cs b/src/Stratis.Features.FederatedPeg/TargetChain/WithdrawalTransactionBuilder.cs index 904c5f2258..2dcb5d5882 100644 --- a/src/Stratis.Features.FederatedPeg/TargetChain/WithdrawalTransactionBuilder.cs +++ b/src/Stratis.Features.FederatedPeg/TargetChain/WithdrawalTransactionBuilder.cs @@ -26,12 +26,14 @@ public class WithdrawalTransactionBuilder : IWithdrawalTransactionBuilder private readonly Network network; private readonly Script cirrusRewardDummyAddressScriptPubKey; + private readonly Script conversionTransactionFeeDistributionScriptPubKey; private readonly IFederationWalletManager federationWalletManager; private readonly IFederationWalletTransactionHandler federationWalletTransactionHandler; private readonly IFederatedPegSettings federatedPegSettings; private readonly ISignals signals; private readonly IRewardDistributionManager distributionManager; private int previousDistributionHeight; + private int previousConversionFeeDistributionHeight; public WithdrawalTransactionBuilder( Network network, @@ -52,7 +54,11 @@ public WithdrawalTransactionBuilder( if (!this.federatedPegSettings.IsMainChain) this.cirrusRewardDummyAddressScriptPubKey = BitcoinAddress.Create(this.network.CirrusRewardDummyAddress).ScriptPubKey; + if (!this.federatedPegSettings.IsMainChain) + this.conversionTransactionFeeDistributionScriptPubKey = BitcoinAddress.Create(this.network.ConversionTransactionFeeDistributionDummyAddress).ScriptPubKey; + this.previousDistributionHeight = 0; + this.previousConversionFeeDistributionHeight = 0; } /// @@ -94,6 +100,17 @@ public Transaction BuildWithdrawalTransaction(int blockHeight, uint256 depositId } } + if (!this.federatedPegSettings.IsMainChain && recipient.ScriptPubKey.Length > 0 && recipient.ScriptPubKey == this.conversionTransactionFeeDistributionScriptPubKey) + { + if (this.previousConversionFeeDistributionHeight != blockHeight) + { + multiSigContext.Recipients = this.distributionManager.DistributeToMultisigNodes(blockHeight, recipient.WithPaymentReducedByFee(FederatedPegSettings.CrossChainTransferFee).Amount); + + // Similarly to the regular distributions, this prevents distribution occurring multiple times in a given block. + this.previousConversionFeeDistributionHeight = blockHeight; + } + } + // TODO: Amend this so we're not picking coins twice. (List coins, List unspentOutputs) = FederationWalletTransactionHandler.DetermineCoins(this.federationWalletManager, this.network, multiSigContext, this.federatedPegSettings); diff --git a/src/Stratis.FullNode.sln b/src/Stratis.FullNode.sln index 591c5c3545..2fc17e2a7d 100644 --- a/src/Stratis.FullNode.sln +++ b/src/Stratis.FullNode.sln @@ -183,6 +183,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stratis.Patricia.Tests", "S EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stratis.Bitcoin.Features.Interop", "Stratis.Bitcoin.Features.Interop\Stratis.Bitcoin.Features.Interop.csproj", "{3DCC6195-1271-4A12-8B94-E821925D98DC}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stratis.Bitcoin.Features.ExternalApi", "Stratis.Bitcoin.Features.ExternalAPI\Stratis.Bitcoin.Features.ExternalApi.csproj", "{74F3F581-03FB-4C18-BED8-D2A526B5138E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stratis.External.Masternodes", "Stratis.External.Masternodes\Stratis.External.Masternodes.csproj", "{F04464B5-9D56-4C9A-9778-27B9D314A296}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -481,6 +485,14 @@ Global {3DCC6195-1271-4A12-8B94-E821925D98DC}.Debug|Any CPU.Build.0 = Debug|Any CPU {3DCC6195-1271-4A12-8B94-E821925D98DC}.Release|Any CPU.ActiveCfg = Release|Any CPU {3DCC6195-1271-4A12-8B94-E821925D98DC}.Release|Any CPU.Build.0 = Release|Any CPU + {74F3F581-03FB-4C18-BED8-D2A526B5138E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74F3F581-03FB-4C18-BED8-D2A526B5138E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74F3F581-03FB-4C18-BED8-D2A526B5138E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74F3F581-03FB-4C18-BED8-D2A526B5138E}.Release|Any CPU.Build.0 = Release|Any CPU + {F04464B5-9D56-4C9A-9778-27B9D314A296}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F04464B5-9D56-4C9A-9778-27B9D314A296}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F04464B5-9D56-4C9A-9778-27B9D314A296}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F04464B5-9D56-4C9A-9778-27B9D314A296}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -549,6 +561,8 @@ Global {57457525-FD6E-401D-BF3F-862F7A2D2AC9} = {1B9A916F-DDAC-4675-B424-EDEDC1A58231} {EE71EBA7-5515-4F1E-B0E0-6C32BAAD6B35} = {1B9A916F-DDAC-4675-B424-EDEDC1A58231} {3DCC6195-1271-4A12-8B94-E821925D98DC} = {15D29FFD-6142-4DC5-AFFD-10BA0CA55C45} + {74F3F581-03FB-4C18-BED8-D2A526B5138E} = {15D29FFD-6142-4DC5-AFFD-10BA0CA55C45} + {F04464B5-9D56-4C9A-9778-27B9D314A296} = {1B9A916F-DDAC-4675-B424-EDEDC1A58231} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6C780ABA-5872-4B83-AD3F-A5BD423AD907} diff --git a/src/Stratis.StraxD/Program.cs b/src/Stratis.StraxD/Program.cs index d16f24fa07..27e233fef0 100644 --- a/src/Stratis.StraxD/Program.cs +++ b/src/Stratis.StraxD/Program.cs @@ -9,6 +9,7 @@ using Stratis.Bitcoin.Features.BlockStore; using Stratis.Bitcoin.Features.ColdStaking; using Stratis.Bitcoin.Features.Consensus; +using Stratis.Bitcoin.Features.ExternalApi; using Stratis.Bitcoin.Features.MemoryPool; using Stratis.Bitcoin.Features.Miner; using Stratis.Bitcoin.Features.RPC; @@ -70,7 +71,8 @@ public static async Task Main(string[] args) }) }; }) - .UseDiagnosticFeature(); + .UseDiagnosticFeature() + .AddExternalApi(); IFullNode node = nodeBuilder.Build(); diff --git a/src/Stratis.StraxD/Stratis.StraxD.csproj b/src/Stratis.StraxD/Stratis.StraxD.csproj index f61eb9e137..bded13d552 100644 --- a/src/Stratis.StraxD/Stratis.StraxD.csproj +++ b/src/Stratis.StraxD/Stratis.StraxD.csproj @@ -26,6 +26,7 @@ +