diff --git a/core/Consensus/Blockmania.cs b/core/Consensus/Blockmania.cs index 186e55b9..7a632db2 100644 --- a/core/Consensus/Blockmania.cs +++ b/core/Consensus/Blockmania.cs @@ -73,7 +73,7 @@ public Blockmania(Config cfg, ILogger logger) // Self, Round, NodeCount, Nodes, TotalNodes, // f, Quorumf1, Quorum2f, Quorum2f1); - _ = Task.Factory.StartNew(async () => { await RunAsync(_entries.Reader); }); + _ = Task.Factory.StartNew(async () => { await Run(_entries.Reader); }); } private void OnBlockmaniaDelivered(InterpretedEventArgs e) @@ -515,12 +515,12 @@ private IEnumerable ProcessMessages(State s, IDictionary /// - private async Task RunAsync(ChannelReader reader) + private async Task Run(ChannelReader reader) { while (await reader.WaitToReadAsync()) while (reader.TryRead(out var data)) { - await _graphMutex.WaitOneAsync(); + _graphMutex.WaitOne(); var entries = new Entry[data.Dependencies.Count]; var max = data.Block.Round; var round = Round; diff --git a/core/CypherNetworkCore.cs b/core/CypherNetworkCore.cs index b579f8e4..faa286c2 100644 --- a/core/CypherNetworkCore.cs +++ b/core/CypherNetworkCore.cs @@ -68,7 +68,7 @@ public class CypherNetworkCore : ICypherNetworkCore private ISync _sync; private IMemoryPool _memoryPool; private IWalletSession _walletSession; - + /// /// /// @@ -151,7 +151,7 @@ public IValidator Validator() } catch (Exception ex) { - _logger.Here().Error("{@Message}",ex.Message); + _logger.Here().Error("{@Message}", ex.Message); } return null; @@ -188,12 +188,12 @@ public INodeWallet Wallet() } catch (Exception ex) { - _logger.Here().Error("{@Message}",ex.Message); + _logger.Here().Error("{@Message}", ex.Message); } return null; } - + /// /// /// @@ -217,7 +217,7 @@ public IBroadcast Broadcast() } catch (Exception ex) { - _logger.Here().Error("{@Message}",ex.Message); + _logger.Here().Error("{@Message}", ex.Message); } return null; @@ -236,7 +236,7 @@ public ICrypto Crypto() } catch (Exception ex) { - _logger.Here().Error("{@Message}",ex.Message); + _logger.Here().Error("{@Message}", ex.Message); } return null; @@ -255,7 +255,7 @@ public IP2PDevice P2PDevice() } catch (Exception ex) { - _logger.Here().Error("{@Message}",ex.Message); + _logger.Here().Error("{@Message}", ex.Message); } return null; @@ -278,11 +278,12 @@ private void Init() var keyPair = AsyncHelper.RunSync(() => crypto.GetOrUpsertKeyNameAsync(AppOptions.Network.SigningKeyRingName)); KeyPair = new KeyPair { - PrivateKey = keyPair.PrivateKey.ByteToHex().ToSecureString(), PublicKey = keyPair.PublicKey + PrivateKey = keyPair.PrivateKey.ByteToHex().ToSecureString(), + PublicKey = keyPair.PublicKey }; keyPair.PrivateKey.Destroy(); } - + /// /// /// @@ -297,7 +298,7 @@ private IUnitOfWork GetUnitOfWork() } catch (Exception ex) { - _logger.Here().Error("{@Message}",ex.Message); + _logger.Here().Error("{@Message}", ex.Message); } return null; @@ -322,7 +323,7 @@ private IPeerDiscovery GetPeerDiscovery() return null; } - + /// /// /// @@ -336,12 +337,12 @@ private IPPoS GetPPoS() } catch (Exception ex) { - _logger.Here().Error("{@Message}",ex.Message); + _logger.Here().Error("{@Message}", ex.Message); } return null; } - + /// /// /// @@ -361,7 +362,7 @@ private IGraph GetGraph() return null; } - + /// /// /// @@ -376,12 +377,12 @@ private ISync GetSync() } catch (Exception ex) { - _logger.Here().Error("{@Message}",ex.Message); + _logger.Here().Error("{@Message}", ex.Message); } return null; } - + /// /// /// @@ -396,12 +397,12 @@ private IMemoryPool GetMemPool() } catch (Exception ex) { - _logger.Here().Error("{@Message}",ex.Message); + _logger.Here().Error("{@Message}", ex.Message); } return null; } - + /// /// /// @@ -416,7 +417,7 @@ private IWalletSession GetWalletSession() } catch (Exception ex) { - _logger.Here().Error("{@Message}",ex.Message); + _logger.Here().Error("{@Message}", ex.Message); } return null; diff --git a/core/Helper/AsyncHelper.cs b/core/Helper/AsyncHelper.cs index 488a3661..43491455 100644 --- a/core/Helper/AsyncHelper.cs +++ b/core/Helper/AsyncHelper.cs @@ -9,9 +9,9 @@ namespace CypherNetwork.Helper; /// public static class AsyncHelper { - private static readonly TaskFactory MyTaskFactory = new(CancellationToken.None, - TaskCreationOptions.None, - TaskContinuationOptions.None, + private static readonly TaskFactory MyTaskFactory = new(CancellationToken.None, + TaskCreationOptions.None, + TaskContinuationOptions.None, TaskScheduler.Default); /// diff --git a/core/Ledger/Graph.cs b/core/Ledger/Graph.cs index b157406c..15f2cbaf 100644 --- a/core/Ledger/Graph.cs +++ b/core/Ledger/Graph.cs @@ -32,7 +32,9 @@ namespace CypherNetwork.Ledger; /// public interface IGraph { - Task GetTransactionBlockIndexAsync(TransactionBlockIndexRequest transactionIndexRequest); + Task GetTransactionBlockIndexAsync( + TransactionBlockIndexRequest transactionIndexRequest); + Task GetTransactionAsync(TransactionRequest transactionRequest); Task GetPreviousBlockAsync(); Task GetSafeguardBlocksAsync(SafeguardBlocksRequest safeguardBlocksRequest); @@ -63,13 +65,13 @@ public sealed class Graph : IGraph, IDisposable private class BlockGraphEventArgs : EventArgs { public BlockGraph BlockGraph { get; } - + public BlockGraphEventArgs(BlockGraph blockGraph) { BlockGraph = blockGraph; } } - + private readonly ActionBlock _action; private readonly ICypherNetworkCore _cypherNetworkCore; private readonly ILogger _logger; @@ -78,10 +80,9 @@ public BlockGraphEventArgs(BlockGraph blockGraph) private readonly Caching _syncCacheBlockGraph = new(); private readonly Caching _syncCacheDelivered = new(); private readonly Caching _syncCacheSeenBlockGraph = new(); - private IDisposable _disposableHandelSeenBlockGraphs; private bool _disposed; - + /// /// private EventHandler _onRoundCompletedEventHandler; @@ -94,8 +95,8 @@ public Graph(ICypherNetworkCore cypherNetworkCore, ILogger logger) { _cypherNetworkCore = cypherNetworkCore; _logger = logger.ForContext("SourceContext", nameof(Graph)); - _onRoundCompleted = Observable.FromEventPattern( - ev => _onRoundCompletedEventHandler += ev, ev => _onRoundCompletedEventHandler -= ev); + _onRoundCompleted = Observable.FromEventPattern(ev => _onRoundCompletedEventHandler += ev, + ev => _onRoundCompletedEventHandler -= ev); _onRoundListener = OnRoundListener(); var dataflowBlockOptions = new ExecutionDataflowBlockOptions { @@ -115,6 +116,7 @@ public async Task PublishAsync(BlockGraph blockGraph) } /// + /// /// /// /// @@ -125,27 +127,25 @@ public async Task GetTransactionBlockIndexAsync( try { var unitOfWork = await _cypherNetworkCore.UnitOfWork(); - var txTransactionOutput = - await unitOfWork.TransactionOutputRepository.GetAsync(transactionIndexRequest.TransactionId); - if (txTransactionOutput is null) return new TransactionBlockIndexResponse(0); - var block = await unitOfWork.HashChainRepository.GetAsync(txTransactionOutput.BlockHash); - if (block is null) return new TransactionBlockIndexResponse(0); - var transaction = block.Txs.FirstOrDefault(x => x.TxnId.Xor(txTransactionOutput.TxId)); - return transaction is null - ? new TransactionBlockIndexResponse(0) - : new TransactionBlockIndexResponse(block.Height); + var block = await unitOfWork.HashChainRepository.GetAsync(x => + new ValueTask(x.Txs.Any(t => t.TxnId.Xor(transactionIndexRequest.TransactionId)))); + if (block is { }) + { + return new TransactionBlockIndexResponse(block.Height); + } } catch (Exception ex) { - _logger.Here().Error("{@Message}", ex.Message); + _logger.Here().Error(ex.Message); } return new TransactionBlockIndexResponse(0); } /// + /// /// - /// + /// /// public async Task GetTransactionAsync(TransactionRequest transactionRequest) { @@ -153,17 +153,18 @@ public async Task GetTransactionAsync(TransactionRequest tr try { var unitOfWork = await _cypherNetworkCore.UnitOfWork(); - var txTransactionOutput = - await unitOfWork.TransactionOutputRepository.GetAsync(transactionRequest.TransactionId); - if (txTransactionOutput is null) return new TransactionResponse(null); - var block = await unitOfWork.HashChainRepository.GetAsync(txTransactionOutput.BlockHash); - if (block is null) return new TransactionResponse(null); - var transaction = block.Txs.FirstOrDefault(x => x.TxnId.Xor(txTransactionOutput.TxId)); - if (transaction is { }) return new TransactionResponse(transaction); + var blocks = await unitOfWork.HashChainRepository.WhereAsync(x => + new ValueTask(x.Txs.Any(t => t.TxnId.Xor(transactionRequest.TransactionId)))); + var block = blocks.FirstOrDefault(); + var transaction = block?.Txs.FirstOrDefault(x => x.TxnId.Xor(transactionRequest.TransactionId)); + if (transaction is { }) + { + return new TransactionResponse(transaction); + } } catch (Exception ex) { - _logger.Here().Error("{@Message}", ex.Message); + _logger.Here().Error(ex.Message); } return new TransactionResponse(null); @@ -232,7 +233,7 @@ public async Task GetBlockCountAsync() { try { - var height = await (await _cypherNetworkCore.UnitOfWork()).HashChainRepository.CountAsync(); + var height = await (await _cypherNetworkCore.UnitOfWork()).HashChainRepository.CountAsync(); return new BlockCountResponse(height); } catch (Exception ex) @@ -254,8 +255,7 @@ public async Task GetBlocksAsync(BlocksRequest blocksRequest) var unitOfWork = await _cypherNetworkCore.UnitOfWork(); var (skip, take) = blocksRequest; var blocks = await unitOfWork.HashChainRepository.OrderByRangeAsync(x => x.Height, skip, take); - if (blocks.Any()) - return new BlocksResponse(blocks); + if (blocks.Any()) return new BlocksResponse(blocks); } catch (Exception ex) { @@ -312,8 +312,7 @@ public async Task BlockHeightExistsAsync(ulong height) { Guard.Argument(height, nameof(height)).NotNegative(); var unitOfWork = await _cypherNetworkCore.UnitOfWork(); - var seen = - await unitOfWork.HashChainRepository.GetAsync(x => new ValueTask(x.Height == height)); + var seen = await unitOfWork.HashChainRepository.GetAsync(x => new ValueTask(x.Height == height)); return seen is not null ? VerifyResult.AlreadyExists : VerifyResult.Succeed; } @@ -325,11 +324,10 @@ public async Task BlockExistsAsync(byte[] hash) { Guard.Argument(hash, nameof(hash)).NotEmpty().NotEmpty().MaxCount(64); var unitOfWork = await _cypherNetworkCore.UnitOfWork(); - var seen = - await unitOfWork.HashChainRepository.GetAsync(x => new ValueTask(x.Hash.Xor(hash))); + var seen = await unitOfWork.HashChainRepository.GetAsync(x => new ValueTask(x.Hash.Xor(hash))); return seen is not null ? VerifyResult.AlreadyExists : VerifyResult.Succeed; } - + /// /// /// @@ -342,7 +340,7 @@ public byte[] HashTransactions(Transaction[] transactions) foreach (var transaction in transactions) ts.Append(transaction.ToStream()); return Hasher.Hash(ts.ToArray()).HexToByte(); } - + /// /// private void Init() @@ -357,8 +355,7 @@ private async Task NewBlockGraphAsync(BlockGraph blockGraph) { Guard.Argument(blockGraph, nameof(blockGraph)).NotNull(); if (blockGraph.Block.Round != await NextRoundAsync()) return; - if (await BlockHeightExistsAsync(blockGraph.Block.Round) != - VerifyResult.Succeed) return; + if (await BlockHeightExistsAsync(blockGraph.Block.Round) != VerifyResult.Succeed) return; if (!_syncCacheSeenBlockGraph.Contains(blockGraph.ToIdentifier())) { _syncCacheSeenBlockGraph.Add(blockGraph.ToIdentifier(), @@ -379,30 +376,29 @@ private void OnRoundReady(BlockGraphEventArgs e) /// private void HandelSeenBlockGraphs() { - _disposableHandelSeenBlockGraphs = Observable.Interval(TimeSpan.FromHours(1)) - .Subscribe(_ => + _disposableHandelSeenBlockGraphs = Observable.Interval(TimeSpan.FromHours(1)).Subscribe(_ => + { + if (_cypherNetworkCore.ApplicationLifetime.ApplicationStopping.IsCancellationRequested) return; + try { - if (_cypherNetworkCore.ApplicationLifetime.ApplicationStopping.IsCancellationRequested) return; - try - { - var removeSeenBlockGraphBeforeTimestamp = Util.GetUtcNow().AddHours(-1).ToUnixTimestamp(); - var removingBlockGraphs = AsyncHelper.RunSync(async delegate - { - return await _syncCacheSeenBlockGraph.WhereAsync(x => - new ValueTask(x.Value.Timestamp < removeSeenBlockGraphBeforeTimestamp)); - }); - foreach (var (key, _) in removingBlockGraphs.OrderBy(x => x.Value.Round)) - _syncCacheSeenBlockGraph.Remove(key); - } - catch (TaskCanceledException) + var removeSeenBlockGraphBeforeTimestamp = Util.GetUtcNow().AddHours(-1).ToUnixTimestamp(); + var removingBlockGraphs = AsyncHelper.RunSync(async delegate { - // Ignore - } - catch (Exception ex) - { - _logger.Here().Error("{@Message}", ex.Message); - } - }); + return await _syncCacheSeenBlockGraph.WhereAsync(x => + new ValueTask(x.Value.Timestamp < removeSeenBlockGraphBeforeTimestamp)); + }); + foreach (var (key, _) in removingBlockGraphs.OrderBy(x => x.Value.Round)) + _syncCacheSeenBlockGraph.Remove(key); + } + catch (TaskCanceledException) + { + // Ignore + } + catch (Exception ex) + { + _logger.Here().Error("{@Message}", ex.Message); + } + }); } /// @@ -412,7 +408,8 @@ private IDisposable OnRoundListener() { var onRoundCompletedSubscription = _onRoundCompleted .Where(data => data.EventArgs.BlockGraph.Block.Round == NextRound()) - .Throttle(TimeSpan.FromSeconds(LedgerConstant.OnRoundThrottleFromSeconds), NewThreadScheduler.Default).Subscribe(_ => + .Throttle(TimeSpan.FromSeconds(LedgerConstant.OnRoundThrottleFromSeconds), NewThreadScheduler.Default) + .Subscribe(_ => { try { @@ -423,8 +420,8 @@ private IDisposable OnRoundListener() var quorum2F1 = 2 * f + 1; if (nodeCount < quorum2F1) return; var lastInterpreted = GetRound(); - var config = new Config(lastInterpreted, Array.Empty(), _cypherNetworkCore.KeyPair.PublicKey.ToHashIdentifier(), - (ulong)nodeCount); + var config = new Config(lastInterpreted, Array.Empty(), + _cypherNetworkCore.KeyPair.PublicKey.ToHashIdentifier(), (ulong)nodeCount); var blockmania = new Blockmania(config, _logger) { NodeCount = nodeCount }; blockmania.TrackingDelivered.Subscribe(x => { @@ -536,7 +533,6 @@ private BlockGraph Copy(BlockGraph blockGraph) Round = blockGraph.Prev.Round } }; - return copy; } catch (Exception ex) @@ -593,8 +589,8 @@ private async Task FinalizeAsync(BlockGraph blockGraph) private async Task OnDeliveredReadyAsync(Interpreted deliver) { Guard.Argument(deliver, nameof(deliver)).NotNull(); - _logger.Information("Delivered: {@Count} Consumed: {@Consumed} Round: {@Round}", - deliver.Blocks.Count, deliver.Consumed, deliver.Round); + _logger.Information("Delivered: {@Count} Consumed: {@Consumed} Round: {@Round}", deliver.Blocks.Count, + deliver.Consumed, deliver.Round); var blocks = deliver.Blocks.Where(x => x.Data is { }); foreach (var deliveredBlock in blocks) try @@ -628,7 +624,6 @@ private async Task DecideWinnerAsync() foreach (var winner in winners) _logger.Here().Information("Hash {@Hash} Solution {@Sol} Node {@Node}", winner.Hash.ByteToHex(), winner.BlockPos.Solution, winner.BlockPos.PublicKey.ToHashIdentifier()); - var block = winners.Length switch { > 2 => winners.FirstOrDefault(winner => @@ -638,7 +633,8 @@ private async Task DecideWinnerAsync() if (block is { }) { if (block.Height != await NextRoundAsync()) return; - if (block.BlockPos.PublicKey.ToHashIdentifier() == (await _cypherNetworkCore.PeerDiscovery()).GetLocalNode().Identifier) + if (block.BlockPos.PublicKey.ToHashIdentifier() == + (await _cypherNetworkCore.PeerDiscovery()).GetLocalNode().Identifier) { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine(@" @@ -647,7 +643,6 @@ _ _ ____ _ _ __ __ _ |_ .. _| | _ \ | | / _ \ / __| | |/ / \ \ /\ / / | | | '_ \ | '_ \ / _ \ | '__| |_ .. _| |_ _| | |_) | | | | (_) | | (__ | < \ V V / | | | | | | | | | | | __/ | | |_ _| |_||_| |____/ |_| \___/ \___| |_|\_\ \_/\_/ |_| |_| |_| |_| |_| \___| |_| |_||_| "); - Console.WriteLine(); Console.ResetColor(); } @@ -655,7 +650,7 @@ _ _ ____ _ _ __ __ _ { _logger.Information("We have a winner {@Hash}", block.Hash.ByteToHex()); } - + if (await BlockHeightExistsAsync(block.Height) == VerifyResult.AlreadyExists) { _logger.Error("Block winner already exists"); @@ -671,19 +666,6 @@ _ _ ____ _ _ __ __ _ var saveBlockResponse = await SaveBlockAsync(new SaveBlockRequest(block)); if (!saveBlockResponse.Ok) _logger.Error("Unable to save the block winner"); - foreach (var (transaction, i) in block.Txs.WithIndex()) - { - var transactionOutput = new TransactionOutput - { - Index = i, - BlockHash = block.Hash, - TxId = transaction.TxnId - }; - - var saved = await (await _cypherNetworkCore.UnitOfWork()).TransactionOutputRepository - .PutAsync(transactionOutput.TxId, transactionOutput); - } - (await _cypherNetworkCore.WalletSession()).Notify(block.Txs.ToArray()); } } @@ -707,7 +689,7 @@ private ulong GetRound() { return AsyncHelper.RunSync(GetRoundAsync); } - + /// /// /// @@ -726,7 +708,7 @@ private ulong NextRound() { return AsyncHelper.RunSync(NextRoundAsync); } - + /// /// /// @@ -754,7 +736,7 @@ await _cypherNetworkCore.Broadcast().PublishAsync((TopicType.AddBlockGraph, _logger.Here().Error(ex, "Broadcast error"); } } - + /// /// /// @@ -774,7 +756,7 @@ private void Dispose(bool disposing) _disposed = true; } - + /// /// /// diff --git a/core/Network/PeerDiscovery.cs b/core/Network/PeerDiscovery.cs index 70d788a6..a4494859 100644 --- a/core/Network/PeerDiscovery.cs +++ b/core/Network/PeerDiscovery.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; @@ -57,7 +58,7 @@ public interface IPeerDiscovery /// public sealed class PeerDiscovery : IDisposable, IPeerDiscovery { - private const int PrunedTimeoutFromSeconds = 300; + private const int PrunedTimeoutFromSeconds = 5000; private const int SurveyorWaitTimeMilliseconds = 3000; private const int ReceiveWaitTimeMilliseconds = 1000; private readonly Caching _caching = new(); @@ -70,7 +71,7 @@ public sealed class PeerDiscovery : IDisposable, IPeerDiscovery private ISurveyorSocket _socket; private ISurveyorAsyncContext _ctx; private bool _disposed; - + private static readonly object LockOnReady = new(); private static readonly object LockOnBootstrap = new(); @@ -124,7 +125,7 @@ private void UpdateLocalPeerInfo() var value = await (await _cypherNetworkCore.Graph()).GetBlockCountAsync(); return value; }); - + _localPeer.BlockCount = (ulong)blockCountResponse.Count; _localPeer.Timestamp = Util.GetAdjustedTimeAsUnixTimestamp(); } @@ -184,10 +185,10 @@ private Task DiscoverAsync() if (_cypherNetworkCore.ApplicationLifetime.ApplicationStopping.IsCancellationRequested) return; StartWorkerAsync(_ctx).Wait(); }); - + return Task.CompletedTask; } - + /// /// /// @@ -213,12 +214,12 @@ private async Task StartWorkerAsync(IReceiveAsyncContext ctx) nngMsg?.Dispose(); } } - + /// /// /// private Task ReceiverAsync() - { + { _receiverDisposable = Observable.Timer(TimeSpan.FromMilliseconds(5000), TimeSpan.FromMilliseconds(1000)).Subscribe(t => { if (_cypherNetworkCore.ApplicationLifetime.ApplicationStopping.IsCancellationRequested) return; @@ -234,7 +235,7 @@ private Task ReceiverAsync() Monitor.Exit(LockOnReady); } }); - + return Task.CompletedTask; } @@ -303,7 +304,7 @@ private async Task BootstrapSeedsAsync() /// /// /// - private static void ReadOnlyPeerSequence(ref IListpeers, ref Sequence sequence) + private static void ReadOnlyPeerSequence(ref IList peers, ref Sequence sequence) { var writer = new MessagePackWriter(sequence); writer.WriteArrayHeader(peers.Count); @@ -313,7 +314,7 @@ private static void ReadOnlyPeerSequence(ref IListpeers, ref Sequence /// /// @@ -391,6 +392,9 @@ private async Task ReceivedPeersAsync(INngMsgPart nngMsg) if (elementSequence == null) continue; var peer = MessagePackSerializer.Deserialize(elementSequence.Value); if (peer.ClientId == _localPeer.ClientId) continue; + if (!IsAcceptedAddress(peer.Advertise)) return; + if (!IsAcceptedAddress(peer.Listening)) return; + if (!IsAcceptedAddress(peer.HttpEndPoint)) return; if (!_caching.TryGet(peer.Advertise, out var cachedPeer)) { _caching.AddOrUpdate(peer.Advertise, peer); @@ -433,6 +437,32 @@ public void TryBootstrap() } } + /// + /// + /// + /// + /// + private static bool IsAcceptedAddress(byte[] value) + { + try + { + var addy = value.FromBytes(); + var p = addy.IndexOf("//", StringComparison.Ordinal); + var a = addy.Substring(p + 2, value.Length - p - 2); + var b = a[..a.IndexOf(":", StringComparison.Ordinal)]; + if (IPAddress.TryParse(b, out var address)) + { + return address.ToString() != "127.0.0.1" && address.ToString() != "0.0.0.0"; + } + } + catch (Exception) + { + // Ignore + } + + return false; + } + /// /// /// @@ -454,7 +484,7 @@ private void Dispose(bool disposing) _disposed = true; } - + /// /// /// diff --git a/core/Persistence/TransactionOutputRepository.cs b/core/Persistence/TransactionOutputRepository.cs deleted file mode 100644 index 7e9a96ee..00000000 --- a/core/Persistence/TransactionOutputRepository.cs +++ /dev/null @@ -1,32 +0,0 @@ -// CypherNetwork by Matthew Hellyer is licensed under CC BY-NC-ND 4.0. -// To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-nd/4.0 - -using CypherNetwork.Models; -using Serilog; - -namespace CypherNetwork.Persistence; - -/// -/// -/// -public interface ITransactionOutputRepository : IRepository -{ - -} - -/// -/// -/// -public class TransactionOutputRepository : Repository, ITransactionOutputRepository -{ - private readonly ILogger _logger; - private readonly IStoreDb _storeDb; - - public TransactionOutputRepository(IStoreDb storeDb, ILogger logger) : base(storeDb, logger) - { - _storeDb = storeDb; - _logger = logger.ForContext("SourceContext", nameof(TransactionOutputRepository)); - - SetTableName(StoreDb.TransactionOutputTable.ToString()); - } -} \ No newline at end of file diff --git a/core/Persistence/UnitOfWork.cs b/core/Persistence/UnitOfWork.cs index db89ed97..f2a568be 100644 --- a/core/Persistence/UnitOfWork.cs +++ b/core/Persistence/UnitOfWork.cs @@ -15,7 +15,6 @@ public interface IUnitOfWork IXmlRepository DataProtectionKeys { get; } IDataProtectionRepository DataProtectionPayload { get; } IHashChainRepository HashChainRepository { get; } - ITransactionOutputRepository TransactionOutputRepository { get; } void Dispose(); } @@ -33,7 +32,6 @@ public UnitOfWork(string folderDb, ILogger logger) var log = logger.ForContext("SourceContext", nameof(UnitOfWork)); DataProtectionPayload = new DataProtectionRepository(StoreDb, log); HashChainRepository = new HashChainRepository(StoreDb, log); - TransactionOutputRepository = new TransactionOutputRepository(StoreDb, log); } public IStoreDb StoreDb { get; } @@ -41,7 +39,6 @@ public UnitOfWork(string folderDb, ILogger logger) public IXmlRepository DataProtectionKeys { get; } public IDataProtectionRepository DataProtectionPayload { get; } public IHashChainRepository HashChainRepository { get; } - public ITransactionOutputRepository TransactionOutputRepository { get; } /// /// diff --git a/core/Wallet/Wallet.cs b/core/Wallet/Wallet.cs index 173e8263..ccb2631b 100644 --- a/core/Wallet/Wallet.cs +++ b/core/Wallet/Wallet.cs @@ -44,7 +44,7 @@ public struct WalletTransaction { public readonly Transaction Transaction; public readonly string Message; - + public WalletTransaction(Transaction transaction, string message) { Transaction = transaction; @@ -118,7 +118,7 @@ public async Task CreateTransactionAsync(ulong amount, ulong return new WalletTransaction(default, $"Unable to create coinstake Commitment: [{verifyOutputCommitments}] Key image: [{verifyKeyImage}]"); } - + foreach (var vout in transaction.Vout) { if (vout.T == CoinType.Coinbase) continue; @@ -226,13 +226,13 @@ private Balance[] GetBalances() .Where(x => !session.CacheConsumed.GetItems().Any(c => x.C.Xor(c.Commit))).ToArray(); if (!outputs.Any()) return Enumerable.Empty().ToArray(); balances.AddRange(from vout in outputs.ToArray() - let coinType = vout.T - where coinType is CoinType.Change or CoinType.Payment or CoinType.Coinstake - let isSpent = IsSpent(vout, session) - where isSpent != true - let amount = Amount(vout, scan) - where amount != 0 - select new Balance { Commitment = vout, Total = amount }); + let coinType = vout.T + where coinType is CoinType.Change or CoinType.Payment or CoinType.Coinstake + let isSpent = IsSpent(vout, session) + where isSpent != true + let amount = Amount(vout, scan) + where amount != 0 + select new Balance { Commitment = vout, Total = amount }); } catch (Exception ex) { @@ -261,7 +261,7 @@ private bool IsSpent(Output output, IWalletSession session) if (result != VerifyResult.Succeed) session.CacheTransactions.Remove(output.C); return result != VerifyResult.Succeed; } - + /// /// /// @@ -347,72 +347,72 @@ private unsafe byte[] RingMembers(ref IWalletSession session, Span blind var transactions = session.GetSafeGuardBlocks() .SelectMany(x => x.Txs).ToArray(); transactions.Shuffle(); - + for (var k = 0; k < nRows - 1; ++k) - for (var i = 0; i < nCols; ++i) - { - if (index == i) + for (var i = 0; i < nCols; ++i) + { + if (index == i) + try + { + var message = Message(session.Spending, scanKey); + var oneTimeSpendKey = spendKey.Uncover(scanKey, new PubKey(session.Spending.E)); + sk[0] = oneTimeSpendKey.ToHex().HexToByte(); + blinds[0] = message.Blind; + pcmIn[i + k * nCols] = pedersen.Commit(message.Amount, message.Blind); + session.CacheConsumed.Add(pcmIn[i + k * nCols], + new Consumed(pcmIn[i + k * nCols], DateTime.UtcNow)); + pkIn[i + k * nCols] = oneTimeSpendKey.PubKey.ToBytes(); + fixed (byte* mm = m, pk = pkIn[i + k * nCols]) + { + Util.MemCpy(&mm[(i + k * nCols) * 33], pk, 33); + } + + continue; + } + catch (Exception) + { + _logger.Here().Error("Unable to create inner ring member"); + return null; + } + try { - var message = Message(session.Spending, scanKey); - var oneTimeSpendKey = spendKey.Uncover(scanKey, new PubKey(session.Spending.E)); - sk[0] = oneTimeSpendKey.ToHex().HexToByte(); - blinds[0] = message.Blind; - pcmIn[i + k * nCols] = pedersen.Commit(message.Amount, message.Blind); - session.CacheConsumed.Add(pcmIn[i + k * nCols], - new Consumed(pcmIn[i + k * nCols], DateTime.UtcNow)); - pkIn[i + k * nCols] = oneTimeSpendKey.PubKey.ToBytes(); + var ringMembers = (from tx in transactions + let vtime = tx.Vtime + where !vtime.IsDefault() + let verifyLockTime = + _cypherNetworkCore.Validator() + .VerifyLockTime(new LockTime(Utils.UnixTimeToDateTime(tx.Vtime.L)), tx.Vtime.S) + where verifyLockTime != VerifyResult.UnableToVerify + select tx).ToArray(); + ringMembers.Shuffle(); + + ringMembers.ElementAt(0).Vout.Shuffle(); + Vout vout; + if (!ContainsCommitment(pcmIn, ringMembers.ElementAt(0).Vout[0].C)) + { + vout = ringMembers.ElementAt(0).Vout[0]; + } + else + { + ringMembers.ElementAt(1).Vout.Shuffle(); + vout = ringMembers.ElementAt(1).Vout[0]; + } + + pcmIn[i + k * nCols] = vout.C; + pkIn[i + k * nCols] = vout.P; + fixed (byte* mm = m, pk = pkIn[i + k * nCols]) { Util.MemCpy(&mm[(i + k * nCols) * 33], pk, 33); } - - continue; } catch (Exception) { - _logger.Here().Error("Unable to create inner ring member"); + _logger.Here().Error("Unable to create outer ring members"); return null; } - - try - { - var ringMembers = (from tx in transactions - let vtime = tx.Vtime - where !vtime.IsDefault() - let verifyLockTime = - _cypherNetworkCore.Validator() - .VerifyLockTime(new LockTime(Utils.UnixTimeToDateTime(tx.Vtime.L)), tx.Vtime.S) - where verifyLockTime != VerifyResult.UnableToVerify - select tx).ToArray(); - ringMembers.Shuffle(); - - ringMembers.ElementAt(0).Vout.Shuffle(); - Vout vout; - if (!ContainsCommitment(pcmIn, ringMembers.ElementAt(0).Vout[0].C)) - { - vout = ringMembers.ElementAt(0).Vout[0]; - } - else - { - ringMembers.ElementAt(1).Vout.Shuffle(); - vout = ringMembers.ElementAt(1).Vout[0]; - } - - pcmIn[i + k * nCols] = vout.C; - pkIn[i + k * nCols] = vout.P; - - fixed (byte* mm = m, pk = pkIn[i + k * nCols]) - { - Util.MemCpy(&mm[(i + k * nCols) * 33], pk, 33); - } - } - catch (Exception) - { - _logger.Here().Error("Unable to create outer ring members"); - return null; } - } return m; } @@ -513,7 +513,8 @@ private TaskResult GenerateTransaction(ref IWalletSession session, _logger.Error("{@Message}", ex.Message); return TaskResult.CreateFailure(JObject.FromObject(new { - success = false, message = ex.Message + success = false, + message = ex.Message })); } } @@ -531,7 +532,7 @@ private byte[] ShortPublicKey() }); return result; } - + /// /// /// @@ -594,13 +595,15 @@ private static TaskResult BulletProof(ulong balance, byte[] blindSu { return TaskResult.CreateFailure(JObject.FromObject(new { - success = false, message = ex.Message + success = false, + message = ex.Message })); } return TaskResult.CreateFailure(JObject.FromObject(new { - success = false, message = "Bulletproof Verify failed" + success = false, + message = "Bulletproof Verify failed" })); } @@ -684,7 +687,7 @@ private static ulong Amount(Output output, Key scan) return 0; } - + /// /// /// diff --git a/core/core.csproj b/core/core.csproj index 6dc7f647..d32ee1d7 100644 --- a/core/core.csproj +++ b/core/core.csproj @@ -4,7 +4,7 @@ net6.0 AnyCPU;x64 CypherNetwork - 0.0.62.0 + 0.0.64.0 CypherNetwork core cyphernetworkcore diff --git a/node/Configuration/Configuration.cs b/node/Configuration/Configuration.cs new file mode 100644 index 00000000..bcec75f8 --- /dev/null +++ b/node/Configuration/Configuration.cs @@ -0,0 +1,51 @@ +using System; +using System.IO; +using CypherNetworkNode.UI; + +namespace CypherNetworkNode.Configuration +{ + public class Configuration + { + private readonly IUserInterface _userInterface; + + public Configuration(IUserInterface userInterface) + { + _userInterface = userInterface; + var networkConfiguration = new Network(userInterface); + if (!networkConfiguration.Do()) + { + Cancel(); + return; + } + + Console.WriteLine("--------------------------------------------------------------------"); + Console.WriteLine("Node name : " + networkConfiguration.Configuration.NodeName); + Console.WriteLine("Public IP address : " + networkConfiguration.Configuration.IpAddress); + Console.WriteLine("Public API port : " + networkConfiguration.Configuration.ApiPortPublic); + Console.WriteLine("Listening public port : " + networkConfiguration.Configuration.ListeningPort); + Console.WriteLine("Advertise public port : " + networkConfiguration.Configuration.AdvertisePort); + Console.WriteLine("--------------------------------------------------------------------"); + Console.WriteLine(); + var configTemplate = File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Configuration", + "Templates", Program.AppSettingsFile)); + var config = configTemplate + .Replace("", + $"http://{networkConfiguration.Configuration.IpAddress}:{networkConfiguration.Configuration.ApiPortPublic.ToString()}") + .Replace("", + $"{networkConfiguration.Configuration.IpAddress}:{networkConfiguration.Configuration.ListeningPort.ToString()}") + .Replace("", + $"{networkConfiguration.Configuration.IpAddress}:{networkConfiguration.Configuration.AdvertisePort.ToString()}") + .Replace("", networkConfiguration.Configuration.NodeName); + var configFileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Program.AppSettingsFile); + File.WriteAllText(configFileName, config); + Console.WriteLine($"Configuration written to {configFileName}"); + Console.WriteLine(); + } + + private void Cancel() + { + var section = new UserInterfaceSection("Cancel configuration", "Configuration cancelled", null); + _userInterface.Do(section); + } + } +} \ No newline at end of file diff --git a/node/Configuration/IPService.cs b/node/Configuration/IPService.cs new file mode 100644 index 00000000..0b7c0fb7 --- /dev/null +++ b/node/Configuration/IPService.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Net; + +namespace CypherNetworkNode.Configuration +{ + public class IPService + { + public IPService(string name, Uri uri) + { + _name = name; + _uri = uri; + } + + private readonly string _name; + private readonly Uri _uri; + + public override string ToString() + { + return $"{_name} ({_uri})"; + } + + public IPAddress Read() + { + using var client = new WebClient(); + var response = client.DownloadString(_uri); + return IPAddress.Parse(response); + } + } + + public class IPServices + { + public static IList Services { get; } = new List() + { + new("ident.me", new Uri("https://v4.ident.me")), + new("ipify.org", new Uri("https://api.ipify.org")), + new("my-ip.io", new Uri("https://api4.my-ip.io/ip.txt")), + new("seeip.org", new Uri("https://ip4.seeip.org")) + }; + } +} \ No newline at end of file diff --git a/node/Configuration/Network.cs b/node/Configuration/Network.cs new file mode 100644 index 00000000..7d8ded56 --- /dev/null +++ b/node/Configuration/Network.cs @@ -0,0 +1,437 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Security.Cryptography; +using CypherNetworkNode.UI; + +namespace CypherNetworkNode.Configuration +{ + public class SerfConfigurationTags + { + public string IPv; + public string ApiPort; + } + + public class SerfConfiguration + { + public string NodeName; + public bool DisableCoordinates; + public readonly SerfConfigurationTags Tags = new(); + public string Bind; + public string Advertise; + public string Profile; + public string RpcAddr; + } + + public class Network + { + private readonly IUserInterface _userInterface; + private readonly UserInterfaceChoice _optionCancel = new(string.Empty); + private readonly IList _ipServices = IPServices.Services; + + public class ConfigurationClass + { + public string NodeName { get; set; } + public IPAddress IpAddress { get; set; } + public ushort ApiPortPublic { get; set; } = 48655; + public ushort ApiPortLocal { get; set; } + public ushort AdvertisePort { get; set; } = 5146; + public ushort ListeningPort { get; set; } = 7946; + } + + public ConfigurationClass Configuration { get; } = new(); + + public Network(IUserInterface userInterface) + { + _userInterface = userInterface.SetTopic("Network"); + } + + public bool Do() + { + return StepIntroduction(); + } + + private bool SetPort(string prompt, out ushort port) + { + var section = new TextInput( + prompt, + (string portString) => ushort.TryParse(portString, out _), + (string portString) => ushort.Parse(portString)); + + return _userInterface.Do(section, out port); + } + + #region Introduction + /// + /// + /// + /// + private bool StepIntroduction() + { + UserInterfaceChoice optionContinue = new("Continue network setup"); + + var section = new UserInterfaceSection( + "Network configuration", + "Cypher nodes communicate with each other directly over tcp. " + + "For a proper node setup, the following " + + "components need to be configured:" + Environment.NewLine + + Environment.NewLine + + " your node remote nodes " + Environment.NewLine + + " ┌─────────────────────────────┐ ┌──────────────────┐" + Environment.NewLine + + " │ ┌───────────┐ │ │ ┌───────────┐ │" + Environment.NewLine + + " │ │ cypnode ◄──────────────┼───────0──┼───► cypnode │ │" + Environment.NewLine + + " │ │ │ │ │ │ │ │ │" + Environment.NewLine + + " │ └─────┬─────┘ │ │ │ └─────┬─────┘ │" + Environment.NewLine + + " │ │ │ │ │ │ │" + Environment.NewLine + + " │ ┌─────▼─────┐ │ │ │ ┌─────▼─────┐ │" + Environment.NewLine + + " │ │ gossip ◄──────────────┼────0──┼──┼───► gossip │ │" + Environment.NewLine + + " │ │ │ │ │ │ │ └───────────┘ │" + Environment.NewLine + + " │ └───────────┘ │ │ │ └──────────────────┘" + Environment.NewLine + + " └─────────────────────────────┘ │ │ ┌──────────────────┐" + Environment.NewLine + + " │ │ │ ┌───────────┐ │" + Environment.NewLine + + " │ └──┼───► cypnode │ │" + Environment.NewLine + + " │ │ └─────┬─────┘ │" + Environment.NewLine + + " │ │ │ │" + Environment.NewLine + + " │ │ ┌─────▼─────┐ │" + Environment.NewLine + + " └─────┼───► gossip │ │" + Environment.NewLine + + " │ └───────────┘ │" + Environment.NewLine + + " └──────────────────┘" + Environment.NewLine, + new[] + { + optionContinue + }); + + var choice = _userInterface.Do(section); + return choice.Equals(optionContinue) && StepNodeName(); + } + + /// + /// + /// + /// + private bool StepNodeName() + { + UserInterfaceChoice optionManualNodeName = new("Manually enter node name"); + UserInterfaceChoice optionGenerateNodeName = new("Automatically generate a node name"); + + var section = new UserInterfaceSection( + "Node name", + "Your node is identified by a name, which can be a human-readable name or a randomly generated name. The node name must be unique in the network.", + new[] + { + optionManualNodeName, + optionGenerateNodeName + }); + + var choiceNodeName = _userInterface.Do(section); + + if (choiceNodeName.Equals(optionManualNodeName)) + { + return StepNodeNameManual(); + } + + if (!choiceNodeName.Equals(optionGenerateNodeName)) return false; + var bytes = new byte[10]; + var randomNumberGenerator = RandomNumberGenerator.Create(); + randomNumberGenerator.GetBytes(bytes); + Configuration.NodeName = $"cypher-{BitConverter.ToString(bytes).Replace("-", "").ToLower()}"; + return StepIpAddress(); + + } + + /// + /// + /// + /// + private bool StepNodeNameManual() + { + var section = new TextInput( + "Enter node name (1 - 32 characters, allowed characters: a-z, A-Z, 0-9, \"_\" and \"-\")", + nodeName => + nodeName.Length >= 1 && + nodeName.Length <= 32 && + nodeName.All(character => char.IsLetterOrDigit(character) || + character.Equals('_') || + character.Equals('-')), + nodeName => nodeName); + + var success = _userInterface.Do(section, out var nodeName); + if (!success) return success; + Configuration.NodeName = nodeName; + return StepIpAddress(); + } + #endregion + + #region IP address + private readonly UserInterfaceChoice _optionIpAddressManual = new("Manually enter IP address"); + private readonly UserInterfaceChoice _optionIpAddressAuto = new("Find IP address automatically"); + private UserInterfaceChoice _choiceIpAddress; + + /// + /// + /// + /// + private bool StepIpAddress() + { + var section = new UserInterfaceSection( + "Public IP address", + "Your node needs to be able to communicate with other nodes on the internet. For this you " + + "need to broadcast your public IP address, which is in many cases not the same as your local network " + + "address. Addresses starting with 10.x.x.x, 172.16.x.x and 192.168.x.x are local addresses and " + + "should not be broadcast to the network. When you do not know your public IP address, you can find " + + "it by searching for 'what is my ip address'. This does not work if you configure a remote node, " + + "like for example a VPS. You can also choose to find your public IP address automatically.", + new[] + { + _optionIpAddressManual, + _optionIpAddressAuto + }); + + _choiceIpAddress = _userInterface.Do(section); + + if (_choiceIpAddress.Equals(_optionIpAddressManual)) + { + return StepIpAddressManual(); + } + return _choiceIpAddress.Equals(_optionIpAddressAuto) && StepIpAddressAuto(); + } + + /// + /// + /// + /// + private bool StepIpAddressManual() + { + var section = new TextInput( + "Enter IP address (e.g. 123.1.23.123)", + ipAddress => IPAddress.TryParse(ipAddress, out _), + ipAddress => IPAddress.Parse(ipAddress)); + + var success = _userInterface.Do(section, out var ipAddress); + if (!success) return success; + Configuration.IpAddress = ipAddress; + return StepApiPortPublic(); + } + + /// + /// + /// + /// + private bool StepIpAddressAuto() + { + while (Configuration.IpAddress == null) + { + var section = new UserInterfaceSection( + _optionIpAddressAuto.Text, + "Please choose the service to use for automatic IP address detection.", + _ipServices.ToList().Select(service => + new UserInterfaceChoice(service.ToString())).ToArray()); + + var choiceIpAddressService = _userInterface.Do(section); + if (choiceIpAddressService.Equals(_optionCancel)) + { + return false; + } + + try + { + var selectedIpAddressService = _ipServices + .First(service => service.ToString() == choiceIpAddressService.Text); + Configuration.IpAddress = selectedIpAddressService.Read(); + } + catch (Exception) + { + // Cannot get IP address; ignore error + } + } + + return StepApiPortPublic(); + } + #endregion IP address + + #region API + private readonly UserInterfaceChoice _optionApiPortDefault = new("Use default port"); + private readonly UserInterfaceChoice _optionApiPortSame = new("Use same local API port as public API port"); + private readonly UserInterfaceChoice _optionApiPortChange = new("Set API port"); + + /// + /// + /// + /// + private bool StepApiPortPublic() + { + var section = new UserInterfaceSection( + "Public API Port", + "Your node exposes an API which needs to be accessible by other nodes in the network. This " + + "API listens on a configurable public TCP port, the default port number is " + + $"{Configuration.ApiPortPublic.ToString()}. You have to make sure that this port is properly " + + "configured in your firewall or router.", + new[] + { + _optionApiPortDefault, + _optionApiPortChange + }); + + var choicePortApi = _userInterface.Do(section); + + if (choicePortApi.Equals(_optionApiPortDefault)) + { + Configuration.ApiPortLocal = Configuration.ApiPortPublic; + return GossipAdvertisePortPublic(); + } + + if (choicePortApi.Equals(_optionApiPortChange)) + { + return StepApiPortPublicSet(); + } + + return false; + } + + /// + /// + /// + /// + private bool StepApiPortPublicSet() + { + var portSet = SetPort("Enter public API port (e.g. 48655)", out var port); + if (!portSet) return false; + + Configuration.ApiPortPublic = port; + return StepApiPortLocal(); + } + + /// + /// + /// + /// + private bool StepApiPortLocal() + { + var section = new UserInterfaceSection( + "Local API Port", + $"In case your node listens on a different local TCP port than the public port you have " + + $"just configured ({Configuration.ApiPortPublic}), for example when you have configured a different " + + "port mapping in your firewall or router, you can set this port here. Most people will want to skip " + + "this step.", + new[] + { + _optionApiPortSame, + _optionApiPortChange + }); + + var choicePortApi = _userInterface.Do(section); + + if (choicePortApi.Equals(_optionApiPortSame)) + { + Configuration.ApiPortLocal = Configuration.ApiPortPublic; + return GossipAdvertisePortPublic(); + } + + if (choicePortApi.Equals(_optionApiPortChange)) + { + return StepApiPortLocalSet(); + } + + return false; + } + + /// + /// + /// + /// + private bool StepApiPortLocalSet() + { + var portSet = SetPort("Enter local API port (e.g. 48655)", out var port); + if (!portSet) return false; + + Configuration.ApiPortLocal = port; + return GossipAdvertisePortPublic(); + } + #endregion API + + #region Gossip + private readonly UserInterfaceChoice _optionPortSerfPublicDefault = new("Use default public Listening port"); + private readonly UserInterfaceChoice _optionPortSerfPublicChange = new("Set public Listening port"); + private readonly UserInterfaceChoice _optionPortSerfRpcDefault = new("Use default Advertise port"); + private readonly UserInterfaceChoice _optionPortSerfRpcChange = new("Set Advertise port"); + /// + /// + /// + /// + private bool GossipAdvertisePortPublic() + { + var section = new UserInterfaceSection( + "Gossip Public Port", + "Node clusters communicate with other nodes " + + $"over a publicly accessible port. By default, Gossip uses port {Configuration.AdvertisePort.ToString()}.", + new[] + { + _optionPortSerfRpcDefault, + _optionPortSerfRpcChange + }); + + var choicePortRpc = _userInterface.Do(section); + + if (choicePortRpc.Equals(_optionPortSerfRpcDefault)) + { + return ListeningPortPublic(); + } + + if (choicePortRpc.Equals(_optionPortSerfRpcChange)) + { + return GossipAdvertisePortPublicSet(); + } + + return false; + } + + private bool GossipAdvertisePortPublicSet() + { + var portSet = SetPort($"Enter Gossip Advertise port (e.g. {Configuration.AdvertisePort.ToString()})", out var port); + if (!portSet) return false; + + Configuration.AdvertisePort = port; + return true; + } + + /// + /// + /// + /// + private bool ListeningPortPublic() + { + var section = new UserInterfaceSection( + "Listening Public Port", + "Nodes and Wallets communicate with your node " + + $"over a publicly accessible port. By default, the Listening uses port {Configuration.ListeningPort.ToString()}.", + new[] + { + _optionPortSerfPublicDefault, + _optionPortSerfPublicChange + }); + + var choicePortPublic = _userInterface.Do(section); + + if (choicePortPublic.Equals(_optionPortSerfPublicDefault)) + { + return true; + } + return choicePortPublic.Equals(_optionPortSerfPublicChange) && GossipListeningPortPublicSet(); + } + + /// + /// + /// + /// + private bool GossipListeningPortPublicSet() + { + var portSet = SetPort($"Enter public Listening port (e.g. {Configuration.ListeningPort.ToString()})", out var port); + if (!portSet) return false; + + Configuration.ListeningPort = port; + return true; + } + #endregion Serf + } +} \ No newline at end of file diff --git a/node/Configuration/Templates/appsettings.json b/node/Configuration/Templates/appsettings.json new file mode 100644 index 00000000..d8e53464 --- /dev/null +++ b/node/Configuration/Templates/appsettings.json @@ -0,0 +1,66 @@ +{ + "Node": { + "Name": "", + "HttpEndPoint": "", + "HttpsPort" : "44333", + "Gossip": { + "Advertise": "", + "Listening": "", + "Seeds": [ + { + "Advertise": "tcp://167.99.81.173:5146", + "Listening": "tcp://167.99.81.173:7946" + } + ] + }, + "Data": { + "rocksdb": "storedb", + "KeysProtectionPath": "keys" + }, + "Staking": { + "TransactionsPerBlock": 133 + }, + "Network": { + "Environment": "testnet", + "SigningKeyRingName": "DefaultSigning.cypnode.Key", + "SyncOnlyWithSeedNodes": false, + "X509Certificate": { + "CertPath": "Cert/certificate.pfx", + "Password": "mypassword", + "Thumbprint": "" + }, + "TransactionRateConfig": { + "LeakRate": 386, + "LeakRateNumberOfSeconds": 5, + "MaxFill": 1024 + } + } + }, + "Log": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "System": "Error", + "Microsoft": "Error" + } + }, + "Enrich": "FromLogContext", + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{MemberName}:{LineNumber}] {Message}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "formatter": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact", + "path": "cyphernetwork.log", + "rollingInterval": "Day", + "retainedFileCountLimit": 7 + } + } + ] + } +} diff --git a/node/Program.cs b/node/Program.cs index 8a5326e7..9eb45ec1 100644 --- a/node/Program.cs +++ b/node/Program.cs @@ -13,7 +13,7 @@ using Autofac.Extensions.DependencyInjection; using Serilog; using CypherNetwork.Helper; -using CypherNetworkNode.Setup; +using CypherNetworkNode.UI; using Microsoft.AspNetCore.DataProtection.XmlEncryption; namespace CypherNetworkNode; @@ -29,7 +29,7 @@ public static class Program /// public static async Task Main(string[] args) { - //args = new string[] { "--configure", "--showkey" }; + //args = new string[] { "--configure", "--help" }; var settingsExists = File.Exists(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppSettingsFile)); if (args.FirstOrDefault(arg => arg == "--configure") != null) { @@ -40,8 +40,9 @@ public static async Task Main(string[] args) } else { - var configSettings = new Config(); - return configSettings.Init(commands.ToArray()); + var ui = new TerminalUserInterface(); + var nc = new Configuration.Configuration(ui); + return 0; } } diff --git a/node/Setup/Config.cs b/node/Setup/Config.cs index 6eca27b8..aa19b2f7 100644 --- a/node/Setup/Config.cs +++ b/node/Setup/Config.cs @@ -29,7 +29,6 @@ public class Config private CommandOption _optionX509CertificatePath; private CommandOption _optionX509CertificatePassword; private CommandOption _optionX509CertificateThumbprint; - private CommandOption _optionStakingEnable; private CommandOption _optionKeyRingName; private class TextInput @@ -130,9 +129,6 @@ public int Init(string[] args) "The password required to access the X.509 certificate data.", CommandOptionType.SingleValue); _optionX509CertificateThumbprint = app.Option("-tcert|--thumbprint ", "The thumbprint (as a hex string) of the certificate to resolve.", CommandOptionType.SingleValue); - _optionStakingEnable = app.Option("-s|--staking ", - "Enable staking if you want to earn rewards. Staking does require that you have some funds available.", - CommandOptionType.NoValue); _optionKeyRingName = app.Option("-rngname|--ringname ", "Replace the existing key ring name with a new default signing name.", CommandOptionType.SingleValue); app.OnExecute(Invoke); @@ -284,11 +280,6 @@ private int Invoke() } } - if (!_optionStakingEnable.HasValue()) return 0; - _appConfigurationOptions.Staking.Enabled = true; - var jTokenStakeEnabled = _jObject.SelectToken("Node.Staking.Enabled"); - jTokenStakeEnabled.Replace(true); - File.WriteAllText(_filePath, JToken.FromObject(_jObject).ToString()); Console.WriteLine("Settings updated"); return 0; diff --git a/node/Startup.cs b/node/Startup.cs index da573ba4..b8e0dc52 100644 --- a/node/Startup.cs +++ b/node/Startup.cs @@ -135,13 +135,13 @@ public void Configure(IApplicationBuilder app, IHostApplicationLifetime lifetime Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("-----------------------------------------------------------------------------------------------"); Console.ForegroundColor = ConsoleColor.Gray; - Console.WriteLine($"| Daemon Cypher private key: {cypherSystem.KeyPair.PrivateKey.FromSecureString()} |"); - Console.WriteLine($"| Daemon Cypher token: {Crypto.GetRandomData().ByteToHex()} |"); + Console.WriteLine($"| Cypher private key: {cypherSystem.KeyPair.PrivateKey.FromSecureString()} |"); + Console.WriteLine($"| Cypher token: {Crypto.GetRandomData().ByteToHex()} |"); Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("-----------------------------------------------------------------------------------------------"); Console.ForegroundColor = ConsoleColor.White; Console.WriteLine(); - Console.WriteLine("Shutting down Daemon Cypher..."); + Console.WriteLine("Shutting down Cypher..."); Environment.Exit(1); return; } diff --git a/node/UI/IUserInterface.cs b/node/UI/IUserInterface.cs new file mode 100644 index 00000000..fd9f5a3a --- /dev/null +++ b/node/UI/IUserInterface.cs @@ -0,0 +1,9 @@ +namespace CypherNetworkNode.UI +{ + public interface IUserInterface + { + public UserInterfaceChoice Do(UserInterfaceSection section); + public bool Do(IUserInterfaceInput input, out T output); + public IUserInterface SetTopic(string topic); + } +} \ No newline at end of file diff --git a/node/UI/IUserInterfaceInput.cs b/node/UI/IUserInterfaceInput.cs new file mode 100644 index 00000000..f779f6b1 --- /dev/null +++ b/node/UI/IUserInterfaceInput.cs @@ -0,0 +1,9 @@ +namespace CypherNetworkNode.UI +{ + public interface IUserInterfaceInput + { + string Prompt { get; } + bool IsValid(string value); + bool Cast(string input, out T output); + } +} \ No newline at end of file diff --git a/node/UI/TerminalUserInterface.cs b/node/UI/TerminalUserInterface.cs new file mode 100644 index 00000000..c5dfcefc --- /dev/null +++ b/node/UI/TerminalUserInterface.cs @@ -0,0 +1,106 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; + +namespace CypherNetworkNode.UI +{ + public class TerminalUserInterface : UserInterfaceBase + { + private const int Indent = 4; + + public override UserInterfaceChoice Do(UserInterfaceSection section) + { + while (true) + { + Console.Clear(); + PrintHeader(section.Title); + Print(section.Description, Indent); + + Console.WriteLine(); + if (section.Choices == null) + { + return null; + } + + for (var choiceIndex = 0; choiceIndex < section.Choices.Length; ++choiceIndex) + { + Print($"{(choiceIndex + 1).ToString()}: {section.Choices[choiceIndex].Text}", 4); + } + Print($"{(section.Choices.Length + 1).ToString()}: Cancel", 4); + + Console.WriteLine(); + + + Console.Write(GetIndentString(Indent)); + var choiceStr = Console.ReadLine(); + if (int.TryParse(choiceStr, out var choiceInt)) + { + if (choiceInt > 0 && choiceInt <= section.Choices.Length) + { + return section.Choices[choiceInt - 1]; + } + + return new UserInterfaceChoice(string.Empty); + } + } + } + + public override bool Do(IUserInterfaceInput input, out T output) + { + output = default; + var validInput = false; + while (!validInput) + { + Console.WriteLine(); + Console.Write($"{GetIndentString(Indent)}{input.Prompt}: "); + + var inputString = Console.ReadLine(); + if (input.IsValid(inputString)) + { + validInput = input.Cast(inputString, out output); + } + } + + return true; + } + + private void PrintHeader(string header) + { + Console.WriteLine($"{_topic} | {header}"); + Console.WriteLine(); + } + + private static void Print(string text, int indent = 0) + { + var lineWidth = Console.WindowWidth - indent; + foreach (var line in text.Split(Environment.NewLine)) + { + var pattern = $@"(?.{{1,{(Console.WindowWidth - indent).ToString()}}})(?.+?)(\s+|$)"; + var lines = Regex.Matches(line, pattern).Select(m => m.Groups["line"].Value); + if (lines.Any()) + { + foreach (var indentLine in lines) + { + Console.WriteLine($"{GetIndentString(Indent)}{indentLine}"); + + } + } + else + { + Console.WriteLine(); + } + } + } + + private static string GetIndentString(int indent) + { + var indentString = string.Empty; + for (var indentIndex = 0; indentIndex < indent; indentIndex++) + { + indentString += ' '; + } + + return indentString; + } + } +} \ No newline at end of file diff --git a/node/UI/TextInput.cs b/node/UI/TextInput.cs new file mode 100644 index 00000000..0337543a --- /dev/null +++ b/node/UI/TextInput.cs @@ -0,0 +1,32 @@ +using System; + +namespace CypherNetworkNode.UI +{ + public class TextInput : IUserInterfaceInput + { + public string Prompt { get; } + private readonly Func _validation; + private readonly Func _cast; + + public TextInput(string prompt, Func validation, Func cast) + { + Prompt = prompt; + _validation = validation; + _cast = cast; + } + + public bool IsValid(string value) + { + return _validation == null || _validation.Invoke(value); + } + + public bool Cast(string input, out T output) + { + output = _cast == null + ? default + : _cast(input); + + return _cast == null || !output.Equals(default); + } + } +} \ No newline at end of file diff --git a/node/UI/UserInterfaceBase.cs b/node/UI/UserInterfaceBase.cs new file mode 100644 index 00000000..e8ad4148 --- /dev/null +++ b/node/UI/UserInterfaceBase.cs @@ -0,0 +1,15 @@ +namespace CypherNetworkNode.UI +{ + public abstract class UserInterfaceBase : IUserInterface + { + public abstract UserInterfaceChoice Do(UserInterfaceSection section); + public abstract bool Do(IUserInterfaceInput input, out T output); + + protected string _topic; + public IUserInterface SetTopic(string topic) + { + _topic = topic; + return this; + } + } +} \ No newline at end of file diff --git a/node/UI/UserInterfaceSection.cs b/node/UI/UserInterfaceSection.cs new file mode 100644 index 00000000..c3f22cc4 --- /dev/null +++ b/node/UI/UserInterfaceSection.cs @@ -0,0 +1,41 @@ +namespace CypherNetworkNode.UI +{ + public class UserInterfaceChoice + { + public UserInterfaceChoice(string text) + { + Text = text; + } + + public string Text { get; } + + public override int GetHashCode() + { + return Text.GetHashCode(); + } + + public override bool Equals(object obj) + { + return Equals(obj as UserInterfaceChoice); + } + + public bool Equals(UserInterfaceChoice otherChoice) + { + return Text == otherChoice.Text; + } + } + + public class UserInterfaceSection + { + public UserInterfaceSection(string title, string description, UserInterfaceChoice[] choices) + { + Title = title; + Description = description; + Choices = choices; + } + + public string Title { get; } + public string Description { get; } + public UserInterfaceChoice[] Choices { get; } + } +} \ No newline at end of file diff --git a/node/node.csproj b/node/node.csproj index 31759d51..abf11bca 100644 --- a/node/node.csproj +++ b/node/node.csproj @@ -2,7 +2,7 @@ true - 0.0.62.0 + 0.0.64.0 en node