From 1782c46db4db8447760bded80f32681371d3d08b Mon Sep 17 00:00:00 2001 From: Maxim Samsonov Date: Tue, 16 Apr 2024 17:33:32 +0300 Subject: [PATCH] Bug fix --- .github/workflows/release.yml | 28 +- README.md | 2 + dkg-nodes.sln | 5 +- dkgCommon/Protos/DkgNode.proto | 6 +- dkgNode/Program.cs | 2 +- dkgNode/Services/DkgNodeServer.cs | 546 ++++++++---------- dkgNode/Services/DkgNodeService.cs | 529 +++++++++++------ dkgNodesTests/ActiveRound.Tests.cs | 92 +++ dkgNodesTests/dkgNodesTests.csproj | 1 + dkgServiceNode/Controllers/NodesController.cs | 2 + .../Controllers/RoundsController.cs | 23 +- dkgServiceNode/Program.cs | 3 + .../Services/RoundRunner/ActiveRound.cs | 198 +++++++ dkgServiceNode/Services/RoundRunner/Runner.cs | 173 ++---- dkgServiceNode/appsettings.Development.json | 2 +- docker-compose.dcproj | 3 - docker-compose.override.yml | 39 -- 17 files changed, 963 insertions(+), 691 deletions(-) create mode 100644 dkgNodesTests/ActiveRound.Tests.cs create mode 100644 dkgServiceNode/Services/RoundRunner/ActiveRound.cs delete mode 100644 docker-compose.override.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e651050..d14d23d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v4 - name: Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + uses: docker/login-action@v3 with: registry: ghcr.io username: maxirmx @@ -38,7 +38,7 @@ jobs: type=sha - name: Build and push Docker image - uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 + uses: docker/build-push-action@v5 with: context: . file: dkgServiceNode/Dockerfile @@ -46,6 +46,16 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + - name: Purge old versions + if: contains(github.ref, 'refs/tags/v') + continue-on-error: true + uses: actions/delete-package-versions@v4 + with: + package-name: dkg-service-node + package-type: 'container' + min-versions-to-keep: 1 + delete-only-untagged-versions: 'true' + release-dkg-node: runs-on: ubuntu-latest permissions: @@ -59,7 +69,7 @@ jobs: submodules: true - name: Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + uses: docker/login-action@v3 with: registry: ghcr.io username: maxirmx @@ -81,10 +91,20 @@ jobs: type=sha - name: Build and push Docker image - uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 + uses: docker/build-push-action@v5 with: context: . file: dkgNode/Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + + - name: Purge old versions + if: contains(github.ref, 'refs/tags/v') + continue-on-error: true + uses: actions/delete-package-versions@v4 + with: + package-name: dkg-node + package-type: 'container' + min-versions-to-keep: 1 + delete-only-untagged-versions: 'true' diff --git a/README.md b/README.md index 7fc1d53..6119572 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # dkg-service-node Dkg service node + +docker run --env=DKG_NODE_SERVER_GRPC_PORT=5050 --env=DKG_NODE_SERVER_GRPC_HOST=localhost --env=DKG_SERVICE_NODE_URL=http://dkg.samsonov.net:8080 --env=DKG_NODE_SERVER_NAME=TestExt --env=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin --env=DOTNET_RUNNING_IN_CONTAINER=true --env=DOTNET_VERSION=8.0.4 --env=ASPNET_VERSION=8.0.4 --workdir=/app -p 5050:5050 --runtime=runc -d ghcr.io/maxirmx/dkg-node:latest \ No newline at end of file diff --git a/dkg-nodes.sln b/dkg-nodes.sln index aab9bdc..53b91b4 100644 --- a/dkg-nodes.sln +++ b/dkg-nodes.sln @@ -16,7 +16,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dkgCommon", "dkgCommon\dkgC EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dkgLibrary", "dkg\dkgLibrary\dkgLibrary.csproj", "{8C58697D-1A09-40EB-B840-CDB9BBE49FA4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dkgNodesTests", "dkgNodesTests\dkgNodesTests.csproj", "{598AFFF1-61B6-43FD-B170-9E44CC6D7EC4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dkgNodesTests", "dkgNodesTests\dkgNodesTests.csproj", "{598AFFF1-61B6-43FD-B170-9E44CC6D7EC4}" + ProjectSection(ProjectDependencies) = postProject + {64F90486-5C7C-4D4B-A1EE-00AAF380B5A7} = {64F90486-5C7C-4D4B-A1EE-00AAF380B5A7} + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/dkgCommon/Protos/DkgNode.proto b/dkgCommon/Protos/DkgNode.proto index 0ef1711..d49e08c 100644 --- a/dkgCommon/Protos/DkgNode.proto +++ b/dkgCommon/Protos/DkgNode.proto @@ -47,7 +47,8 @@ message PublicKeyReply { } message ProcessDealRequest { - bytes data = 1; + int32 roundId = 1; + bytes data = 2; } message ProcessDealReply { @@ -55,7 +56,8 @@ message ProcessDealReply { } message ProcessResponseRequest { - bytes data = 1; + int32 roundId = 1; + bytes data = 2; } message ProcessResponseReply { diff --git a/dkgNode/Program.cs b/dkgNode/Program.cs index 5edcad8..da749c2 100644 --- a/dkgNode/Program.cs +++ b/dkgNode/Program.cs @@ -19,7 +19,7 @@ var loggerFactory = app.Services.GetRequiredService(); var logger = loggerFactory.CreateLogger("DkgNode"); -var server = new DkgNodeServer(config, serviceNodeUrl, logger); +var server = new DkgNodeService(config, serviceNodeUrl, logger); var cts = new CancellationTokenSource(); AssemblyLoadContext.Default.Unloading += ctx => diff --git a/dkgNode/Services/DkgNodeServer.cs b/dkgNode/Services/DkgNodeServer.cs index 8503142..86593a7 100644 --- a/dkgNode/Services/DkgNodeServer.cs +++ b/dkgNode/Services/DkgNodeServer.cs @@ -1,397 +1,307 @@ -// Copyright (C) 2024 Maxim [maxirmx] Samsonov (www.sw.consulting) -// All rights reserved. -// This file is a part of dkg service node -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions -// are met: -// 1. Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// 2. Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in the -// documentation and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED -// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS -// BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. - -using dkg.group; -using dkgNode.Models; -using Grpc.Core; -using System.Text.Json; -using System.Text; +using dkg.group; +using dkg.poly; +using dkg.share; using dkgNode.Constants; -using static dkgNode.Constants.NStatus; +using Google.Protobuf; +using Grpc.Core; +using dkgCommon; +using dkgNode.Models; using static dkgCommon.DkgNode; +using static dkgNode.Constants.NStatus; +using Org.BouncyCastle.Asn1.Ocsp; +using static Org.BouncyCastle.Math.EC.ECCurve; +using dkg.vss; -using dkgCommon.Models; -using dkg.share; -using dkg; -using dkgCommon; -using Google.Protobuf; -using Grpc.Net.Client; namespace dkgNode.Services { - // Узел - // Создаёт instance gRPC сервера (class DkgNodeServer) - // и gRPC клиента (это просто отдельный поток TheThread) - // В TheThread реализована незатейливая логика этого примера - class DkgNodeServer + // gRPC сервер + // Здесь "сложены" параметры узла, которые нужны и клиенту и серверу + class DkgNodeServer : DkgNodeBase { - internal JsonSerializerOptions JsonSerializerOptions = new() { PropertyNameCaseInsensitive = true }; - internal Server GRpcServer { get; } - internal DkgNodeService DkgNodeSrv { get; } - - // Публичныке ключи других участников - internal IPoint[] PublicKeys { get; set; } = []; - - internal Thread RunnerThread { get; set; } - internal bool IsRunning { get; set; } = true; - - internal bool ContinueDkg + private IGroup G { get; } + internal string Name { get; } + internal IScalar PrivateKey { get; } // Приватный ключ этого узла + internal IPoint PublicKey { get; } // Публичный ключ этого узла + internal PriShare? SecretShare { get; set; } = null; + internal DkgNodeConfig[] Configs { get; set; } = []; + + // Distributed Key Generator + public DistKeyGenerator? Dkg { get; set; } = null; + // Защищает Dkg от параллельной обработки наскольких запросов + private readonly object dkgLock = new() { }; + + // Node status + private NStatus Status { get; set; } = NotRegistered; + private int? Round { get; set; } = null; + private IPoint? DistributedPublicKey = null; // Distributed public key + private readonly object stsLock = new() { }; + + public void SetStatus(NStatus status) { - get { return Status == Running && IsRunning; } + lock (stsLock) + { + Status = status; + } } - internal NStatus Status + public void SetStatusAndRound(NStatus status, int round) { - get { return DkgNodeSrv.GetStatus(); } - set { DkgNodeSrv.SetStatus(value); } + lock (stsLock) + { + Status = status; + Round = round; + } } - internal IPoint? DistributedPublicKey + public NStatus GetStatus() { - get { return DkgNodeSrv.GetDistributedPublicKey(); } - set { DkgNodeSrv.SetDistributedPublicKey(value); } + lock (stsLock) + { + return Status; + } } - internal IGroup G { get; } - internal ILogger Logger { get; } - internal string ServiceNodeUrl { get; } - DkgNodeConfig Config { get; } - DkgNodeConfig[] Configs + public int? GetRound() { - get { return DkgNodeSrv.Configs; } + lock (stsLock) + { + return Round; + } } - public byte[] PublicKey + + public void SetDistributedPublicKey(IPoint? dpk) { - get { return DkgNodeSrv.PublicKey.GetBytes(); } + lock (stsLock) + { + DistributedPublicKey = dpk; + } } - public string Name + public IPoint? GetDistributedPublicKey() { - get { return DkgNodeSrv.Name; } + IPoint? dpk = null; + lock (stsLock) + { + dpk = DistributedPublicKey; + } + return dpk; } - internal async Task Register(HttpClient httpClient) + // Cipher + internal IPoint C1 { get; set; } + internal IPoint C2 { get; set; } + + private readonly ILogger _logger; + public DkgNodeServer(ILogger logger, string name, IGroup group) { - int? roundId = null; - HttpResponseMessage? response = null; - var jsonPayload = JsonSerializer.Serialize(Config); - var httpContent = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); + _logger = logger; + G = group; + C1 = G.Point(); + C2 = G.Point(); + Name = name; + PrivateKey = G.Scalar(); + PublicKey = G.Base().Mul(PrivateKey); + } - try - { - response = await httpClient.PostAsync(ServiceNodeUrl + "/api/nodes/register", httpContent); - } - catch (Exception e) - { - Logger.LogError($"Node '{Config.Name}' failed to register with {ServiceNodeUrl}, Exception: {e.Message}"); - } - if (response == null) + + // gRPC сервер реализует 4 метода + // + // Выдача публичного ключа + // ProcessDeal + // ProcessResponse + // Прием сообщения + // Частичная расшифровка + public override Task GetPublicKey(PublicKeyRequest _, ServerCallContext context) + { + PublicKeyReply resp = new() { Data = ByteString.CopyFrom(PublicKey.GetBytes()) }; + return Task.FromResult(resp); + } + + public override Task ProcessDeal(ProcessDealRequest deal, ServerCallContext context) + { + ProcessDealReply resp; + + bool proceed = false; + lock (stsLock) { - Logger.LogError($"Node '{Config.Name}' failed to register with {ServiceNodeUrl}, no response received"); + if (deal.RoundId == Round) + { + proceed = true; + } } - else + + if (proceed) { - var responseContent = await response.Content.ReadAsStringAsync(); + DistDeal distDeal = new(); + distDeal.SetBytes(deal.Data.ToByteArray()); - if (response.IsSuccessStatusCode) + lock (dkgLock) { - try + ByteString data = ByteString.CopyFrom([]); + if (Dkg != null) { - Reference? reference = JsonSerializer.Deserialize(responseContent, JsonSerializerOptions); - if (reference == null) + try { - Logger.LogError($"Node '{Config.Name}' failed to parse service node response '{responseContent}' from {ServiceNodeUrl}"); + data = ByteString.CopyFrom(Dkg.ProcessDeal(distDeal).GetBytes()); + _logger.LogDebug("'{Name}': ProcessDeal request [Round: {RoundId}]", Name, Round); } - else + catch (Exception ex) { - if (reference.Id == 0) - { - roundId = null; - Logger.LogInformation($"Node '{Config.Name}' not registered with {ServiceNodeUrl} - no rounds"); - } - else - { - roundId = reference.Id; - Logger.LogInformation($"Node '{Config.Name}' succesfully registered with {ServiceNodeUrl}"); - } + // Ошибки на данном этапе не являются фатальными + // Если response'а нет, это просто значит, что в дальнейшую обработку ничего не уйдёт. + _logger.LogDebug("'{Name}': ProcessDeal request failed [Round: {RoundId}]\n, {Message}", Name, Round, ex.Message); } } - catch (JsonException ex) - { - Logger.LogError($"Node '{Config.Name}' failed to parse service node response '{responseContent}' from {ServiceNodeUrl}"); - Logger.LogError(ex.Message); - } - } - else - { - Logger.LogError($"Node '{Config.Name}' failed to register with {ServiceNodeUrl}: {response.StatusCode}"); - Logger.LogError(responseContent); + resp = new ProcessDealReply { Data = data }; } } - return roundId; + else + { + resp = new ProcessDealReply(); + _logger.LogError("'{Name}': ProcessDeal request Id mismatch [Node round: {Round}, Request round: {RoundId}]", + Name, Round, deal.RoundId); + } + + return Task.FromResult(resp); } - internal async void Runner() + public override Task ProcessResponse(ProcessResponseRequest response, ServerCallContext context) { - var httpClient = new HttpClient(); - int j = 0; - while (IsRunning) + bool proceed = false; + lock (stsLock) { - if (Status == NotRegistered) + if (response.RoundId == Round) { - int? roundId = await Register(httpClient); - if (roundId != null) - { - DkgNodeSrv.SetStatusAndRound(WaitingRoundStart, (int)roundId); - } + proceed = true; } + } - if (Status == Running) - { - RunDkg(); - } - else + if (proceed) + { + DistResponse distResponse = new(); + distResponse.SetBytes(response.Data.ToByteArray()); + + lock (dkgLock) { - if (j++ % 30 == 0) + ByteString data = ByteString.CopyFrom([]); + if (Dkg != null) { - Logger.LogDebug($" '{Config.Name}': '{NodeStatusConstants.GetRoundStatusById(Status).Name}'"); + try + { + DistJustification? distJust = Dkg.ProcessResponse(distResponse); + string anno = "no justification"; + if (distJust != null) + { + anno = "with jsutification"; + } + // data = ByteString.CopyFrom(distJust.GetBytes()); + _logger.LogDebug("'{Name}': ProcessResponse request [Round: {RoundId}, {anno}]", Name, Round, anno); + } + catch (Exception ex) + { + // Ошибки на данном этапе не являются фатальными + // Если response не удалось обработать, это значит, что он не учитывается. Как будто и не было. + _logger.LogDebug("'{Name}': ProcessResponse request failed [Round: {RoundId}]\n, {Message}", Name, Round, ex.Message); + } } - Thread.Sleep(1000); } } - } - public DkgNodeServer(DkgNodeConfig config, string serviceNodeUrl, ILogger logger) - { - Config = config; - Logger = logger; - ServiceNodeUrl = serviceNodeUrl; - - logger.LogInformation($"Starting '{Config.Name}' host: {Config.Host}, port: {Config.Port}"); - - G = new Secp256k1Group(); - - DkgNodeSrv = new DkgNodeService(logger, Config.Name, G); - Config.PublicKey = Convert.ToBase64String(PublicKey); - - GRpcServer = new Server + else { - Services = { BindService(DkgNodeSrv) }, - Ports = { new ServerPort("0.0.0.0", Config.Port, ServerCredentials.Insecure) } - }; - - RunnerThread = new Thread(Runner); - } - - public void Start() - { - - Logger.LogInformation($"Starting '{Config.Name}'"); - GRpcServer.Start(); - RunnerThread.Start(); - } - - public void Shutdown() - { - GRpcServer.ShutdownAsync().Wait(); - IsRunning = false; - RunnerThread.Join(); + _logger.LogError("'{Name}': ProcessResponse request Id mismatch [Node round: {Round}, Request round: {RoundId}]", + Name, Round, response.RoundId); + } + return Task.FromResult(new ProcessResponseReply()); } - // gRPC клиент и драйвер всего процесса - public void RunDkg() + public override Task RunRound(RunRoundRequest request, ServerCallContext context) { - Logger.LogDebug($"'{Config.Name}': Running Dkg algorithm for {Configs.Length} nodes: step 1"); - // gRPC клиенты "в сторону" других участников - // включая самого себя, чтобы было меньше if'ов - GrpcChannel[] Channels = new GrpcChannel[Configs.Length]; - DkgNodeClient[] Clients = new DkgNodeClient[Configs.Length]; - - for (int j = 0; j < Configs.Length; j++) + bool res = false; + DkgNodeConfig[] configs = request.DkgNodeRefs.Select(nodeRef => new DkgNodeConfig { - Channels[j] = GrpcChannel.ForAddress($"http://{Configs[j].Host}:{Configs[j].Port}"); - Clients[j] = new DkgNodeClient(Channels[j]); // ChannelCredentials.Insecure ??? - } + Port = nodeRef.Port, + Host = nodeRef.Host, + PublicKey = nodeRef.PublicKey, + }).ToArray(); - // Таймаут, который используется в точках синхронизации вместо синхронизации - int syncTimeout = Math.Max(10000, Configs.Length * 1000); - - PublicKeys = new IPoint[Configs.Length]; - - // Пороговое значение для верификации ключа, то есть сколько нужно валидных commitment'ов - // Алгоритм Шамира допускает минимальное значение = N/2+1, где N - количество участников, но мы - // cделаем N-1, так чтобы 1 неадекватная нода позволяла расшифровать сообщение, а две - нет. - int threshold = PublicKeys.Length/2 + 1; - - // 1. Собираем публичные ключи со всех участников - // Тут, конечно, упрощение. Предполагается, что все ответят без ошибкт - // В промышленном варианте список участников, который у нас есть - это список желательных участников - // В этом уикле нужно сформировать список реальных кчастников, то есть тех, где gRPC end point хотя бы - // откликается - for (int j = 0; j < Configs.Length; j++) + lock (stsLock) { - byte[] pkb = []; - var pk = Clients[j].GetPublicKey(new PublicKeyRequest()); - if (pk != null) - { - pkb = pk.Data.ToByteArray(); - } - if (pkb.Length != 0) + if (request.RoundId == Round) { - PublicKeys[j] = G.Point().SetBytes(pkb); - // Console.WriteLine($"Got public key of node {j} at node {Index}: {PublicKeys[j]}"); - } - else - { - // См. комментарий выше - // PubliсKeys[j] = null не позволит инициализировать узел - // Можно перестроить список участников, можно использовать "левый" - // Для демо считаем это фатальной ошибкой - Logger.LogError($"FATAL ERROR FOR NODE '{Config.Name}': failed to get public key of node '{Configs[j].Name}'"); - Status = Failed; + Status = Running; + Configs = configs; + res = true; } } - // Здесь будут distributed deals (не знаю, как перевести), предложенные этим узлом другим узлам - // <индекс другого узла> --> наш deal для другого узла - Dictionary deals = []; - - if (ContinueDkg) + if (!res) { - // Дадим время всем другим узлам обменяться публичными ключами - // Можно добавить точку синхронизации, то есть отдельным gRPC вызовом опрашивать вскх участников дошли ли они до этой точки, - // но тогда возникает вопром, что делать с теми кто до неё не доходит "никогда" (в смысле "достаточно быстро") - Logger.LogDebug($"'{Config.Name}': Running Dkg algorithm for {Configs.Length} nodes: step 2"); - Thread.Sleep(syncTimeout); - - // 2. Создаём генератор/обработчик распределённого ключа для этого узла - // Это будет DkgNode.Dkg. Он создаётся уровнем ниже, чтобы быть доступным как из gRPC клиента (этот объект), - // так и из сервера (DkgNode) + _logger.LogError("'{Name}': RunRound request Id mismatch [Node round: {Round}, Request round: {RoundId}]", Name, Round, request.RoundId); + } + else + { + _logger.LogInformation("'{Name}': RunRound request [Round: {request.RoundId}]", Name, request.RoundId); + } + return Task.FromResult(new RunRoundReply() { Res = res }); + } - try - { - DkgNodeSrv.Dkg = DistKeyGenerator.CreateDistKeyGenerator(G, DkgNodeSrv.PrivateKey, PublicKeys, threshold) ?? - throw new Exception($"Could not create distributed key generator/handler"); - deals = DkgNodeSrv.Dkg.GetDistDeals() ?? - throw new Exception($"Could not get a list of deals"); - } - // Исключение может быть явно созданное выше, а может "выпасть" из DistKeyGenerator - // Ошибки здесь все фатальны - catch (Exception ex) + public override Task EndRound(EndRoundRequest request, ServerCallContext context) + { + bool res = false; + lock (stsLock) + { + if (request.RoundId == Round) { - Logger.LogError($"FATAL ERROR FOR NODE '{Config.Name}': {ex.Message}"); - Status = Failed; + Status = NotRegistered; + Round = null; + DistributedPublicKey = null; + res = true; } } - DistKeyShare? distrKey = null; - IPoint? distrPublicKey = null; + EndRoundReply endRoundReply = new EndRoundReply() { Res = res }; - // 3. Разошkём наши "предложения" другим узлам - // В ответ мы ожидаем distributed response, который мы для начала сохраним + if (!res) + { + _logger.LogError("'{Name}': EndRound request Id mismatch [Node round: {Round}, Request round: {RoundId}]", Name, Round, request.RoundId); + } + else + { + _logger.LogInformation("'{Name}': EndRound request [Round: {request.RoundId}]", Name, request.RoundId); + } + return Task.FromResult(endRoundReply); + } - if (ContinueDkg) + public override Task RoundResult(RoundResultRequest request, ServerCallContext context) + { + bool res = false; + IPoint? dpk = null; + lock (stsLock) { - List responses = new(deals.Count); - foreach (var (i, deal) in deals) + if (request.RoundId == Round) { - // Console.WriteLine($"Querying from {Index} to process for node {i}"); - - byte[] rspb = []; - // Самому себе тоже пошлём, хотя можно вызвать локально - // if (Index == i) try { response = DkgNode.Dkg!.ProcessDeal(response) } catch { } - var rb = Clients[i].ProcessDeal(new ProcessDealRequest { Data = ByteString.CopyFrom(deal.GetBytes()) }); - if (rb != null) - { - rspb = rb.Data.ToByteArray(); - } - if (rspb.Length != 0) - { - DistResponse response = new(); - response.SetBytes(rspb); - responses.Add(response); - } - else - { - // На этом этапе ошибка не является фатальной - // Просто у нас или получится или не получится достаточное количество commitment'ов - // См. комментариё выше про Threshold - Logger.LogDebug($"Node '{Config.Name}': failed to get response from node '{Configs[i].Name}'"); - } + dpk = DistributedPublicKey; + res = true; } + } - if (ContinueDkg) - { - // Тут опять точка синхронизации - // Участник должен сперва получить deal, а только потом response'ы для этого deal - // В противном случае response будет проигнорирован - // Можно передать ошибку через gRPC, анализировать в цикле выше и вызывать ProcessResponse повторно. - // Однако, опять вопрос с теми, кто не ответит никогда. - Logger.LogDebug($"'{Config.Name}': Running Dkg algorithm for {Configs.Length} nodes: step 3"); - Thread.Sleep(syncTimeout); + RoundResultReply roundResultReply = new RoundResultReply() { Res = (res && dpk != null) }; - foreach (var response in responses) - { - for (int i = 0; i < PublicKeys.Length; i++) - { - // Самому себе тоже пошлём, хотя можно вызвать локально - // if (Index == i) try { DkgNode.Dkg!.ProcessResponse(response) } catch { } - Clients[i].ProcessResponse(new ProcessResponseRequest { Data = ByteString.CopyFrom(response.GetBytes()) }); - } - } - } - - if (ContinueDkg) + if (!res) + { + _logger.LogError("'{Name}': RoundResult request Id mismatch [Node round: {Round}, Request round: {RoundId}]", Name, Round, request.RoundId); + } + else + { + string anno = "no result"; + if (dpk != null) { - // И ещё одна точка синхронизации - // Теперь мы ждём, пока все обменяются responsе'ами - Logger.LogDebug($"'{Config.Name}': Running Dkg algorithm for {Configs.Length} nodes: step 4"); - Thread.Sleep(syncTimeout); - - DkgNodeSrv.Dkg!.SetTimeout(); - - // Обрадуемся тому, что нас признали достойными :) - bool crt = DkgNodeSrv.Dkg!.ThresholdCertified(); - string certified = crt ? "" : "not "; - Logger.LogDebug($"'{Config.Name}': {certified}certified"); - - if (crt) - { - // Методы ниже безопасно вызывать, только если ThresholdCertified() вернул true - distrKey = DkgNodeSrv.Dkg!.DistKeyShare(); - DkgNodeSrv.SecretShare = distrKey.PriShare(); - distrPublicKey = distrKey.Public(); - DistributedPublicKey = distrPublicKey; - Status = Finished; - } - else - { - DistributedPublicKey = null; - Status = Failed; - } + roundResultReply.DistributedPublicKey = ByteString.CopyFrom(dpk.GetBytes()); + anno = "result"; } + _logger.LogDebug("'{Name}' RoundResult request [Round: {RoundId}] returning {anno}", Name, request.RoundId, anno); } + return Task.FromResult(roundResultReply); } - } -} \ No newline at end of file +} diff --git a/dkgNode/Services/DkgNodeService.cs b/dkgNode/Services/DkgNodeService.cs index 0be07d6..aabdb6e 100644 --- a/dkgNode/Services/DkgNodeService.cs +++ b/dkgNode/Services/DkgNodeService.cs @@ -1,261 +1,418 @@ -using dkg.group; -using dkg.poly; -using dkg.share; -using dkgNode.Constants; -using Google.Protobuf; -using Grpc.Core; -using dkgCommon; +// Copyright (C) 2024 Maxim [maxirmx] Samsonov (www.sw.consulting) +// All rights reserved. +// This file is a part of dkg service node +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS +// BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +using dkg.group; using dkgNode.Models; +using Grpc.Core; +using System.Text.Json; +using System.Text; +using dkgNode.Constants; +using static dkgNode.Constants.NStatus; using static dkgCommon.DkgNode; -using static dkgNode.Constants.NStatus; +using dkgCommon.Models; +using dkg.share; +using dkg; +using dkgCommon; +using Google.Protobuf; +using Grpc.Net.Client; namespace dkgNode.Services { - // gRPC сервер - // Здесь "сложены" параметры узла, которые нужны и клиенту и серверу - class DkgNodeService : DkgNodeBase + // Узел + // Создаёт instance gRPC сервера (class DkgNodeServer) + // и gRPC клиента (это просто отдельный поток TheThread) + // В TheThread реализована незатейливая логика этого примера + class DkgNodeService { - private IGroup G { get; } - internal string Name { get; } - internal IScalar PrivateKey { get; } // Приватный ключ этого узла - internal IPoint PublicKey { get; } // Публичный ключ этого узла - internal PriShare? SecretShare { get; set; } = null; - internal DkgNodeConfig[] Configs { get; set; } = []; - - // Distributed Key Generator - public DistKeyGenerator? Dkg { get; set; } = null; - // Защищает Dkg от параллельной обработки наскольких запросов - private readonly object dkgLock = new() { }; - - // Node status - private NStatus Status { get; set; } = NotRegistered; - private int? Round { get; set; } = null; - private IPoint? DistributedPublicKey = null; // Distributed public key - private readonly object stsLock = new() { }; - - public void SetStatus(NStatus status) + internal JsonSerializerOptions JsonSerializerOptions = new() { PropertyNameCaseInsensitive = true }; + internal Server GRpcServer { get; } + internal DkgNodeServer DkgNodeSrv { get; } + + // Публичныке ключи других участников + internal IPoint[] PublicKeys { get; set; } = []; + + internal Thread RunnerThread { get; set; } + internal bool IsRunning { get; set; } = true; + + internal bool ContinueDkg { - lock (stsLock) - { - Status = status; - } + get { return Status == Running && IsRunning; } } - public void SetStatusAndRound(NStatus status, int round) + internal NStatus Status { - lock (stsLock) - { - Status = status; - Round = round; - } + get { return DkgNodeSrv.GetStatus(); } + set { DkgNodeSrv.SetStatus(value); } } - public NStatus GetStatus() + internal IPoint? DistributedPublicKey { - lock (stsLock) - { - return Status; - } + get { return DkgNodeSrv.GetDistributedPublicKey(); } + set { DkgNodeSrv.SetDistributedPublicKey(value); } } - public void SetDistributedPublicKey(IPoint? dpk) + internal int? Round { - lock (stsLock) - { - DistributedPublicKey = dpk; - } + get { return DkgNodeSrv.GetRound(); } } + internal IGroup G { get; } - public IPoint? GetDistributedPublicKey() + internal ILogger Logger { get; } + internal string ServiceNodeUrl { get; } + DkgNodeConfig Config { get; } + DkgNodeConfig[] Configs { - IPoint? dpk = null; - lock (stsLock) - { - dpk = DistributedPublicKey; - } - return dpk; + get { return DkgNodeSrv.Configs; } } - - // Cipher - internal IPoint C1 { get; set; } - internal IPoint C2 { get; set; } - - private readonly ILogger _logger; - public DkgNodeService(ILogger logger, string name, IGroup group) + public byte[] PublicKey { - _logger = logger; - G = group; - C1 = G.Point(); - C2 = G.Point(); - Name = name; - PrivateKey = G.Scalar(); - PublicKey = G.Base().Mul(PrivateKey); + get { return DkgNodeSrv.PublicKey.GetBytes(); } } - - // gRPC сервер реализует 4 метода - // - // Выдача публичного ключа - // ProcessDeal - // ProcessResponse - // Прием сообщения - // Частичная расшифровка - public override Task GetPublicKey(PublicKeyRequest _, ServerCallContext context) + public string Name { - PublicKeyReply resp = new() { Data = ByteString.CopyFrom(PublicKey.GetBytes()) }; - return Task.FromResult(resp); + get { return DkgNodeSrv.Name; } } - public override Task ProcessDeal(ProcessDealRequest deal, ServerCallContext context) + internal async Task Register(HttpClient httpClient) { - ProcessDealReply resp; - - DistDeal distDeal = new(); - distDeal.SetBytes(deal.Data.ToByteArray()); + int? roundId = null; + HttpResponseMessage? response = null; + var jsonPayload = JsonSerializer.Serialize(Config); + var httpContent = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); - lock (dkgLock) + try { - ByteString data = ByteString.CopyFrom([]); - if (Dkg != null) + response = await httpClient.PostAsync(ServiceNodeUrl + "/api/nodes/register", httpContent); + } + catch (Exception e) + { + Logger.LogError("'{Name}': failed to register with {ServiceNodeUrl}, Exception: {Message}", + Config.Name, ServiceNodeUrl, e.Message); + } + if (response == null) + { + Logger.LogError("Node '{Name}' failed to register with {ServiceNodeUrl}, no response received", + Config.Name, ServiceNodeUrl); + } + else + { + var responseContent = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) { try { - data = ByteString.CopyFrom(Dkg.ProcessDeal(distDeal).GetBytes()); + Reference? reference = JsonSerializer.Deserialize(responseContent, JsonSerializerOptions); + if (reference == null) + { + Logger.LogError("'{Name}': failed to parse service node response '{responseContent}' from {ServiceNodeUrl}", + Config.Name, responseContent, ServiceNodeUrl); + } + else + { + if (reference.Id == 0) + { + roundId = null; + Logger.LogDebug("'{Name}': registered with {ServiceNodeUrl} [No round]", + Config.Name, ServiceNodeUrl); + } + else + { + roundId = reference.Id; + Logger.LogInformation("'{Name}': registered with {ServiceNodeUrl} [Round {roundId}]", + Config.Name, ServiceNodeUrl, roundId); + } + } } - catch (Exception ex) + catch (JsonException ex) { - // Ошибки на данном этапе не являются фатальными - // Если response'а нет, это просто значит, что в дальнейшую обработку ничего не уйдёт. - Console.WriteLine($"{Name}: {ex.Message}"); + Logger.LogError("'{Name}': failed to parse service node response '{responseContent}' from {ServiceNodeUrl}", + Config.Name, responseContent, ServiceNodeUrl); + Logger.LogError(ex.Message); } } - - resp = new ProcessDealReply { Data = data }; + else + { + Logger.LogError("'{Name}': failed to register with {ServiceNodeUrl}: {StatusCode}", + Config.Name, ServiceNodeUrl, response.StatusCode); + Logger.LogError(responseContent); + } } - return Task.FromResult(resp); + return roundId; } - public override Task ProcessResponse(ProcessResponseRequest response, ServerCallContext context) + internal async void Runner() { - DistResponse distResponse = new(); - distResponse.SetBytes(response.Data.ToByteArray()); - - lock (dkgLock) + var httpClient = new HttpClient(); + while (IsRunning) { - ByteString data = ByteString.CopyFrom([]); - if (Dkg != null) + if (Status == NotRegistered) { - try - { - DistJustification? distJust = Dkg.ProcessResponse(distResponse); - if (distJust != null) - Console.WriteLine($"{Name}: justification !!!"); - // data = ByteString.CopyFrom(distJust.GetBytes()); - } - catch (Exception ex) + int? roundId = await Register(httpClient); + if (roundId != null) { - // Ошибки на данном этапе не являются фатальными - // Если response не удалось обработать, это значит, что он не учитывается. Как будто и не было. - Console.WriteLine($"{Name}: {ex.Message}"); + DkgNodeSrv.SetStatusAndRound(WaitingRoundStart, (int)roundId); } } + + if (Status == Running) + { + RunDkg(); + } + else + { + Logger.LogDebug("'{Name}': '{StatusName}'", + Name, NodeStatusConstants.GetRoundStatusById(Status).Name); + Thread.Sleep(3000); + } } - return Task.FromResult(new ProcessResponseReply()); + } + public DkgNodeService(DkgNodeConfig config, string serviceNodeUrl, ILogger logger) + { + Config = config; + Logger = logger; + ServiceNodeUrl = serviceNodeUrl; + + logger.LogInformation("'{Name}': starting at {Config.Host}:{Config.Port}", + Config.Name, Config.Host, Config.Port); + G = new Secp256k1Group(); + + DkgNodeSrv = new DkgNodeServer(logger, Config.Name, G); + Config.PublicKey = Convert.ToBase64String(PublicKey); + + GRpcServer = new Server + { + Services = { BindService(DkgNodeSrv) }, + Ports = { new ServerPort("0.0.0.0", Config.Port, ServerCredentials.Insecure) } + }; + + RunnerThread = new Thread(Runner); } - public override Task RunRound(RunRoundRequest request, ServerCallContext context) + public void Start() { - bool res = false; - DkgNodeConfig[] configs = request.DkgNodeRefs.Select(nodeRef => new DkgNodeConfig + + Logger.LogInformation("'{Name}': Start", Config.Name); + GRpcServer.Start(); + RunnerThread.Start(); + } + + public void Shutdown() + { + GRpcServer.ShutdownAsync().Wait(); + IsRunning = false; + RunnerThread.Join(); + Logger.LogInformation("'{Name}': Shutdown", Config.Name); + } + + // gRPC клиент и драйвер всего процесса + public void RunDkg() + { + Logger.LogDebug("'{Name}': Running Dkg algorithm for {Length} nodes [Round {round}, step 1]", + Config.Name, Configs.Length, Round); + // gRPC клиенты "в сторону" других участников + // включая самого себя, чтобы было меньше if'ов + GrpcChannel[] Channels = new GrpcChannel[Configs.Length]; + DkgNodeClient[] Clients = new DkgNodeClient[Configs.Length]; + + for (int j = 0; j < Configs.Length; j++) { - Port = nodeRef.Port, - Host = nodeRef.Host, - PublicKey = nodeRef.PublicKey, - }).ToArray(); + Channels[j] = GrpcChannel.ForAddress($"http://{Configs[j].Host}:{Configs[j].Port}"); + Clients[j] = new DkgNodeClient(Channels[j]); // ChannelCredentials.Insecure ??? + } + + // Таймаут, который используется в точках синхронизации вместо синхронизации + int syncTimeout = Math.Max(10000, Configs.Length * 1000); - lock (stsLock) + PublicKeys = new IPoint[Configs.Length]; + + // Пороговое значение для верификации ключа, то есть сколько нужно валидных commitment'ов + // Алгоритм Шамира допускает минимальное значение = N/2+1, где N - количество участников, но мы + // cделаем N-1, так чтобы 1 неадекватная нода позволяла расшифровать сообщение, а две - нет. + int threshold = PublicKeys.Length/2 + 1; + + // 1. Декодируем публичные ключи со для вчех участников + // Тут, конечно, упрощение. Предполагается, что все ответят без ошибoк + // В промышленном варианте список участников, который у нас есть - это список желательных участников + // В этом цикле нужно сформировать список реальных участников, то есть тех, где gRPC end point хотя бы + // откликается + for (int j = 0; j < Configs.Length; j++) { - if (request.RoundId == Round) + byte[] pkb = []; + var pk = Configs[j].PublicKey; + if (pk != null) + { + pkb = Convert.FromBase64String(pk); + } + if (pkb.Length != 0) { - Status = Running; - Configs = configs; - res = true; + PublicKeys[j] = G.Point().SetBytes(pkb); + } + else + { + // См. комментарий выше + // PubliсKeys[j] = null не позволит инициализировать узел + // Можно перестроить список участников, можно использовать "левый" + // Пока считаем это фатальной ошибкой + Logger.LogError("'{Name}': NODE FATAL ERROR, failed to get public key of node '{OtherName}'", + Config.Name, Configs[j].Name); + Status = Failed; } } - if (!res) - { - _logger.LogError($"RunRound request failed for '{Name}' [Node round: {Round}, Request round: {request.RoundId}]"); - } - else + // Здесь будут distributed deals (не знаю, как перевести), предложенные этим узлом другим узлам + // <индекс другого узла> --> наш deal для другого узла + Dictionary deals = []; + + if (ContinueDkg) { - _logger.LogDebug($"RunRound request executed for '{Name}' [Round: {request.RoundId}]"); - } + // Дадим время всем другим узлам обменяться публичными ключами + // Можно добавить точку синхронизации, то есть отдельным gRPC вызовом опрашивать вскх участников дошли ли они до этой точки, + // но тогда возникает вопром, что делать с теми кто до неё не доходит "никогда" (в смысле "достаточно быстро") + Logger.LogDebug("'{Name}': Running Dkg algorithm for {Length} nodes [Round {round}, step 2]", + Config.Name, Configs.Length, Round); + Thread.Sleep(syncTimeout); - return Task.FromResult(new RunRoundReply() { Res = res }); - } + // 2. Создаём генератор/обработчик распределённого ключа для этого узла + // Это будет DkgNode.Dkg. Он создаётся уровнем ниже, чтобы быть доступным как из gRPC клиента (этот объект), + // так и из сервера (DkgNode) - public override Task EndRound(EndRoundRequest request, ServerCallContext context) - { - bool res = false; - lock (stsLock) - { - if (request.RoundId == Round) + try { - Status = NotRegistered; - Round = null; - DistributedPublicKey = null; - res = true; + DkgNodeSrv.Dkg = DistKeyGenerator.CreateDistKeyGenerator(G, DkgNodeSrv.PrivateKey, PublicKeys, threshold) ?? + throw new Exception($"Could not create distributed key generator/handler"); + deals = DkgNodeSrv.Dkg.GetDistDeals() ?? + throw new Exception($"Could not get a list of deals"); + } + // Исключение может быть явно созданное выше, а может "выпасть" из DistKeyGenerator + // Ошибки здесь все фатальны + catch (Exception ex) + { + Logger.LogError("'{Name}': NODE FATAL ERROR\n{Message}", Config.Name, ex.Message); + Status = Failed; } } - EndRoundReply endRoundReply = new EndRoundReply() { Res = res }; + DistKeyShare? distrKey = null; + IPoint? distrPublicKey = null; - if (!res) - { - _logger.LogError($"EndRound request failed for '{Name}' [Node round: {Round}, Request round: {request.RoundId}]"); - } - else - { - _logger.LogDebug($"EndRound request executed for '{Name}' [Round: {request.RoundId}]"); - } - return Task.FromResult(endRoundReply); - } + // 3. Разошkём наши "предложения" другим узлам + // В ответ мы ожидаем distributed response, который мы для начала сохраним - public override Task RoundResult(RoundResultRequest request, ServerCallContext context) - { - bool res = false; - IPoint? dpk = null; - byte[] distributedPublicKey = []; - lock (stsLock) + if (ContinueDkg) { - if (request.RoundId == Round) + List responses = new(deals.Count); + foreach (var (i, deal) in deals) { - dpk = DistributedPublicKey; - res = true; + // Console.WriteLine($"Querying from {Index} to process for node {i}"); + + byte[] rspb = []; + // Самому себе тоже пошлём, хотя можно вызвать локально + // if (Index == i) try { response = DkgNode.Dkg!.ProcessDeal(response) } catch { } + var rb = Clients[i].ProcessDeal(new ProcessDealRequest { + RoundId = (int)(Round == null ? 0 : Round), + Data = ByteString.CopyFrom(deal.GetBytes()) + }); + if (rb != null) + { + rspb = rb.Data.ToByteArray(); + } + if (rspb.Length != 0) + { + DistResponse response = new(); + response.SetBytes(rspb); + responses.Add(response); + } + else + { + // На этом этапе ошибка не является фатальной + // Просто у нас или получится или не получится достаточное количество commitment'ов + // См. комментариё выше про Threshold + Logger.LogDebug("'{Name}': failed to get response from node '{OtherName}'", + Config.Name, Configs[i].Name); + } } - } - if (res && dpk == null) - { - _logger.LogError($"RoundResult request succeeded '{Name}' but distributed public key is not set, round: {request.RoundId}]"); - res = false; - } + if (ContinueDkg) + { + // Тут опять точка синхронизации + // Участник должен сперва получить deal, а только потом response'ы для этого deal + // В противном случае response будет проигнорирован + // Можно передать ошибку через gRPC, анализировать в цикле выше и вызывать ProcessResponse повторно. + // Однако, опять вопрос с теми, кто не ответит никогда. + Logger.LogDebug("'{Name}': Running Dkg algorithm for {Length} nodes [Round {round}, step 3]", + Config.Name, Configs.Length, Round); + Thread.Sleep(syncTimeout); - RoundResultReply roundResultReply = new RoundResultReply() { Res = res }; + foreach (var response in responses) + { + for (int i = 0; i < PublicKeys.Length; i++) + { + // Самому себе тоже пошлём, хотя можно вызвать локально + // if (Index == i) try { DkgNode.Dkg!.ProcessResponse(response) } catch { } + Clients[i].ProcessResponse(new ProcessResponseRequest { + RoundId = (int)(Round == null ? 0: Round), + Data = ByteString.CopyFrom(response.GetBytes()) + }); + } + } + } - if (!res) - { - _logger.LogError($"RoundResult request failed for '{Name}' [Node round: {Round}, Request round: {request.RoundId}]"); - } - else - { - distributedPublicKey = dpk!.GetBytes(); - roundResultReply.DistributedPublicKey = ByteString.CopyFrom(distributedPublicKey); - _logger.LogDebug($"RoundResult request executed for '{Name}' [Round: {request.RoundId}]"); + if (ContinueDkg) + { + // И ещё одна точка синхронизации + // Теперь мы ждём, пока все обменяются responsе'ами + Logger.LogDebug("'{Name}': Running Dkg algorithm for {Length} nodes [Round {round}, step 4]", + Config.Name, Configs.Length, Round); + Thread.Sleep(syncTimeout); + + DkgNodeSrv.Dkg!.SetTimeout(); + + // Обрадуемся тому, что нас признали достойными :) + bool crt = DkgNodeSrv.Dkg!.ThresholdCertified(); + string certified = crt ? "" : "not "; + Logger.LogInformation("'{Name}': {certified}certified", Config.Name, certified); + + if (crt) + { + // Методы ниже безопасно вызывать, только если ThresholdCertified() вернул true + distrKey = DkgNodeSrv.Dkg!.DistKeyShare(); + DkgNodeSrv.SecretShare = distrKey.PriShare(); + distrPublicKey = distrKey.Public(); + DistributedPublicKey = distrPublicKey; + Status = Finished; + } + else + { + DistributedPublicKey = null; + Status = Failed; + } + } } - return Task.FromResult(roundResultReply); } } -} +} \ No newline at end of file diff --git a/dkgNodesTests/ActiveRound.Tests.cs b/dkgNodesTests/ActiveRound.Tests.cs new file mode 100644 index 0000000..a07bd7b --- /dev/null +++ b/dkgNodesTests/ActiveRound.Tests.cs @@ -0,0 +1,92 @@ +using Moq; +using Microsoft.Extensions.Logging; +using dkgServiceNode.Services.RoundRunner; +using dkgServiceNode.Models; + +namespace dkgNodesTests +{ + + [TestFixture] + public class ActiveRoundTests + { + private Mock> _loggerMock; + + [SetUp] + public void SetUp() + { + _loggerMock = new Mock>(); + } + + [Test] + public void Constructor_Sets_Id() + { + var round = new Round { Id = 5 }; + var activeRound = new ActiveRound(round, _loggerMock.Object); + Assert.That(activeRound.Id, Is.EqualTo(round.Id)); + } + + [Test] + public void Run_WithEmptyNodes_DoesNotThrow() + { + var round = new Round { Id = 5 }; + var activeRound = new ActiveRound(round, _loggerMock.Object); + var nodes = new List(); + + + Assert.DoesNotThrow(() => activeRound.Run(nodes)); + } + + [Test] + public void Run_WithNodes_CallsRunRound() + { + var round = new Round { Id = 5 }; + var activeRound = new ActiveRound(round, _loggerMock.Object); + var nodes = new List { new() { Host = "localhost", Port = 5000 } }; + + activeRound.Run(nodes); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains("Run")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + [Test] + public void GetResult_WithNodes_ReturnsResult() + { + var round = new Round { Id = 5 }; + var activeRound = new ActiveRound(round, _loggerMock.Object); + var nodes = new List { new() { Host = "localhost", Port = 5000 } }; + + activeRound.Run(nodes); + var result = activeRound.GetResult(); + + Assert.That(result, Is.Null); + } + + [Test] + public void Clear_WithNodes_CallsClearInternal() + { + var round = new Round { Id = 5 }; + var activeRound = new ActiveRound(round, _loggerMock.Object); + var nodes = new List { new() { Host = "localhost", Port = 5000 } }; + + activeRound.Run(nodes); + activeRound.Clear(nodes); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains("ClearInternal")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + } +} \ No newline at end of file diff --git a/dkgNodesTests/dkgNodesTests.csproj b/dkgNodesTests/dkgNodesTests.csproj index 336cb4c..cea2691 100644 --- a/dkgNodesTests/dkgNodesTests.csproj +++ b/dkgNodesTests/dkgNodesTests.csproj @@ -12,6 +12,7 @@ + diff --git a/dkgServiceNode/Controllers/NodesController.cs b/dkgServiceNode/Controllers/NodesController.cs index 5875f1f..f0db649 100644 --- a/dkgServiceNode/Controllers/NodesController.cs +++ b/dkgServiceNode/Controllers/NodesController.cs @@ -108,7 +108,9 @@ public async Task> RegisterNode(Node node) roundId = 0; } var reference = new Reference((int)roundId); + return Ok(reference); + } // DELETE: api/nodes/5 diff --git a/dkgServiceNode/Controllers/RoundsController.cs b/dkgServiceNode/Controllers/RoundsController.cs index 2e5052a..f6cb847 100644 --- a/dkgServiceNode/Controllers/RoundsController.cs +++ b/dkgServiceNode/Controllers/RoundsController.cs @@ -44,12 +44,14 @@ public class RoundsController : DControllerBase { protected readonly RoundContext roundContext; protected readonly NodeContext nodeContext; + protected readonly Runner runner; - public RoundsController(IHttpContextAccessor httpContextAccessor, UserContext uContext, RoundContext rContext, NodeContext nContext) : + public RoundsController(IHttpContextAccessor httpContextAccessor, UserContext uContext, RoundContext rContext, NodeContext nContext, Runner rnner) : base(httpContextAccessor, uContext) { roundContext = rContext; nodeContext = nContext; + runner = rnner; } // GET: api/rounds @@ -76,11 +78,7 @@ public async Task> GetRound(int id) { var round = await roundContext.Rounds.FindAsync(id); if (round == null) return _404Round(id); - - if (round.IsVersatile) - { - round.NodeCount = await nodeContext.Nodes.CountAsync(n => n.RoundId == round.Id); - } + round.NodeCount = await nodeContext.Nodes.CountAsync(n => n.RoundId == round.Id); return round; } @@ -120,28 +118,29 @@ public async Task> NextRoundStep(int id) round.CreatedOn = round.CreatedOn.ToUniversalTime(); round.Status = round.NextStatus; + var rNodes = await nodeContext.Nodes.Where(n => n.RoundId == round.Id).ToListAsync(); if (round.IsVersatile) { - round.NodeCount = await nodeContext.Nodes.CountAsync(n => n.RoundId == round.Id); + round.NodeCount = rNodes.Count; } switch (round.StatusValue) { case (short)RStatus.Started: - Runner.StartRound(round); + runner.StartRound(round); break; case (short)RStatus.Running: - Runner.RunRound(round, await nodeContext.Nodes.ToListAsync()); + runner.RunRound(round, rNodes); break; case (short)RStatus.Finished: - round.Result = Runner.FinishRound(round, await nodeContext.Nodes.ToListAsync()); + round.Result = runner.FinishRound(round, rNodes); if (round.Result == null) { round.StatusValue = (short)RStatus.Failed; } break; case (short)RStatus.Cancelled: - Runner.CancelRound(round, await nodeContext.Nodes.ToListAsync()); + runner.CancelRound(round, rNodes); break; default: break; @@ -201,7 +200,7 @@ public async Task> CancelRound(int id) throw; } } - Runner.CancelRound(round, await nodeContext.Nodes.ToListAsync()); + runner.CancelRound(round, await nodeContext.Nodes.ToListAsync()); return NoContent(); } } diff --git a/dkgServiceNode/Program.cs b/dkgServiceNode/Program.cs index 636de1f..5a53c59 100644 --- a/dkgServiceNode/Program.cs +++ b/dkgServiceNode/Program.cs @@ -2,6 +2,7 @@ using dkgServiceNode.Data; using dkgServiceNode.Services.Authorization; +using dkgServiceNode.Services.RoundRunner; var builder = WebApplication.CreateBuilder(args); @@ -31,6 +32,8 @@ builder.Services.AddDbContext(options => options.UseNpgsql(connectionString)); builder.Services.AddDbContext(options => options.UseNpgsql(connectionString)); +builder.Services.AddSingleton(); + var app = builder.Build(); app.UseCors(x => x diff --git a/dkgServiceNode/Services/RoundRunner/ActiveRound.cs b/dkgServiceNode/Services/RoundRunner/ActiveRound.cs new file mode 100644 index 0000000..f3b0a0f --- /dev/null +++ b/dkgServiceNode/Services/RoundRunner/ActiveRound.cs @@ -0,0 +1,198 @@ +// Copyright (C) 2024 Maxim [maxirmx] Samsonov (www.sw.consulting) +// All rights reserved. +// This file is a part of dkg service node +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS +// BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +using Grpc.Net.Client; + +using dkgCommon; +using dkgServiceNode.Models; +using static dkgCommon.DkgNode; + + +namespace dkgServiceNode.Services.RoundRunner +{ + public class ActiveRound + { + private GrpcChannel[]? _channels { get; set; } = null; + private DkgNodeClient[]? _dkgNodes { get; set; } = null; + private Round _round { get; set; } + private readonly ILogger _logger; + private static readonly object lockObject = new(); + public ActiveRound(Round round, ILogger logger) + { + _logger = logger; + _round = round; + + _logger.LogDebug("Round [{Id}]: Created", Id); + } + public int Id + { + get { return _round.Id; } + } + private void InitInternal(List nodes) + { + _logger.LogDebug("Round [{Id}]: InitInternal", Id); + _channels = new GrpcChannel[nodes.Count]; + _dkgNodes = new DkgNodeClient[nodes.Count]; + int j = 0; + foreach (Node node in nodes) + { + string dest = $"http://{node.Host}:{node.Port}"; + _channels[j] = GrpcChannel.ForAddress(dest); + _dkgNodes[j] = new DkgNodeClient(_channels[j]); // ChannelCredentials.Insecure ??? + _logger.LogDebug("Round [{Id}]: Created channel to {dest}", Id, dest); + j++; + } + _logger.LogDebug("Round [{Id}]: InitInternal completed", Id); + } + + private void ClearInternal() + { + _logger.LogDebug("Round [{Id}]: ClearInternal", Id); + + List shutdownTasks = []; + try + { + for (int j = 0; j < _dkgNodes?.Length; j++) + { + shutdownTasks.Add(_dkgNodes[j].EndRoundAsync(new EndRoundRequest { RoundId = Id }).ResponseAsync); + } + Task.WaitAll([.. shutdownTasks]); + } + catch (Exception ex) + { + _logger.LogError("Round [{Id}]: ClearInternal exception at EndRound\n{message}", Id, ex.Message); + } + + shutdownTasks.Clear(); + try + { + for (int i = 0; i < _channels?.Length; i++) + { + shutdownTasks.Add(_channels[i].ShutdownAsync()); + } + Task.WaitAll([.. shutdownTasks]); + } + catch (Exception ex) + { + _logger.LogError("Round [{Id}]: ClearInternal exception at Shutdown\n{message}", Id, ex.Message); + } + _logger.LogDebug("Round [{Id}]: ClearInternal completed", Id); + } + + public void Run(List nodes) + { + _logger.LogDebug("Round [{Id}]: Run for {count} nodes", Id, _dkgNodes?.Length); + lock (lockObject) + { + InitInternal(nodes); + + try + { + var runRoundRequest = new RunRoundRequest + { + RoundId = Id + }; + + foreach (var node in nodes) + { + runRoundRequest.DkgNodeRefs.Add(new DkgNodeRef + { + Port = node.Port, + Host = node.Host, + PublicKey = node.PublicKey, + }); + } + + + List startRounsTasks = []; + for (int j = 0; j < _dkgNodes?.Length; j++) + { + startRounsTasks.Add(_dkgNodes[j].RunRoundAsync(runRoundRequest).ResponseAsync); + } + + Task.WaitAll([.. startRounsTasks]); + } + catch (Exception ex) + { + _logger.LogError("Round [{Id}]: Run exception at RunRound\n{message}", Id, ex.Message); + } + _logger.LogDebug("Round [{Id}]: Run completed", Id); + } + } + + public int? GetResult() + { + int? result = null; + + _logger.LogDebug("Round [{Id}]: GetResult", Id); + lock (lockObject) + { + if (_dkgNodes != null) + { + try + { + foreach (var dkgNode in _dkgNodes) + { + var roundResultRequest = new RoundResultRequest + { + RoundId = _round.Id + }; + + RoundResultReply roundResultReply = dkgNode.RoundResult(roundResultRequest); + + if (roundResultReply.Res) + { + result = BitConverter.ToInt32(roundResultReply.DistributedPublicKey.ToByteArray(), 0); + break; + } + } + } + catch (Exception ex) + { + _logger.LogError("Round [{Id}]: GetResult terminating with exception\n{message}", Id, ex.Message); + } + } + } + _logger.LogDebug("Round [{Id}]: GetResult returning {result}", Id, result); + + return result; + } + + public void Clear(List? nodes) + { + _logger.LogDebug("Round [{Id}]: Clear", Id); + lock (lockObject) + { + if (_dkgNodes == null && nodes != null) + { + InitInternal(nodes); + } + ClearInternal(); + } + _logger.LogDebug("Round [{Id}]: Clear completed", Id); + } + + } +} diff --git a/dkgServiceNode/Services/RoundRunner/Runner.cs b/dkgServiceNode/Services/RoundRunner/Runner.cs index c1b8959..e1ab5a6 100644 --- a/dkgServiceNode/Services/RoundRunner/Runner.cs +++ b/dkgServiceNode/Services/RoundRunner/Runner.cs @@ -1,142 +1,74 @@ -using dkgCommon; -using dkgServiceNode.Models; +// Copyright (C) 2024 Maxim [maxirmx] Samsonov (www.sw.consulting) +// All rights reserved. +// This file is a part of dkg service node +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS +// BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + using Grpc.Net.Client; + +using dkgCommon; +using dkgServiceNode.Models; using static dkgCommon.DkgNode; namespace dkgServiceNode.Services.RoundRunner { - public class ActiveRound + public class Runner { - public int Id - { - get { return Round.Id; } - } - public GrpcChannel[]? Channels { get; set; } = null; - public DkgNodeClient[]? DkgNodes { get; set; } = null; - internal Round Round { get; set; } - public ActiveRound(Round round) - { - Round = round; - } - - public void Init(List nodes) - { - Channels = new GrpcChannel[nodes.Count]; - DkgNodes = new DkgNodeClient[nodes.Count]; - int j = 0; - foreach (Node node in nodes) - { - Channels[j] = GrpcChannel.ForAddress($"http://{node.Host}:{node.Port}"); - DkgNodes[j] = new DkgNodeClient(Channels[j]); // ChannelCredentials.Insecure ??? - j++; - } - } - - public void Run(List nodes) - { - Init(nodes); - var runRoundRequest = new RunRoundRequest - { - RoundId = Id - }; - - foreach (var node in nodes) - { - runRoundRequest.DkgNodeRefs.Add(new DkgNodeRef - { - Port = node.Port, - Host = node.Host, - PublicKey = node.PublicKey, - }); - } - - List startRounsTasks = []; - for (int j = 0; j < DkgNodes?.Length; j++) - { - startRounsTasks.Add(DkgNodes[j].RunRoundAsync(runRoundRequest).ResponseAsync); - } - - Task.WaitAll([.. startRounsTasks]); - - } - - public int? GetResult() - { - if (DkgNodes == null) - { - return null; - } - - foreach (var dkgNode in DkgNodes) - { - var roundResultRequest = new RoundResultRequest - { - RoundId = Round.Id - }; - - RoundResultReply roundResultReply = dkgNode.RoundResult(roundResultRequest); + private readonly ILogger _logger; + private List ActiveRounds { get; set; } = []; + private readonly object lockObject = new(); - if (roundResultReply.Res) - { - int value = BitConverter.ToInt32(roundResultReply.DistributedPublicKey.ToByteArray(), 0); - return value; - } - } - - return null; - } - public void Clear() + public Runner(ILogger logger) { - List shutdownTasks = []; - for (int j = 0; j < DkgNodes?.Length; j++) - { - shutdownTasks.Add(DkgNodes[j].EndRoundAsync(new EndRoundRequest { RoundId = Id }).ResponseAsync); - } - Task.WaitAll([.. shutdownTasks]); - - shutdownTasks.Clear(); - for (int i = 0; i < Channels?.Length; i++) - { - shutdownTasks.Add(Channels[i].ShutdownAsync()); - } - Task.WaitAll([.. shutdownTasks]); + _logger = logger; } - } - - - public static class Runner - { - private static List ActiveRounds { get; set; } = []; - private static readonly object lockObject = new(); - - public static void StartRound(Round round) + public void StartRound(Round round) { lock (lockObject) { - ActiveRounds.Add(new ActiveRound(round)); + ActiveRounds.Add(new ActiveRound(round, _logger)); } } - public static void RunRound(Round round, List? nodes) + public void RunRound(Round round, List? nodes) { ActiveRound? roundToRun = null; lock (lockObject) { - roundToRun = ActiveRounds.FirstOrDefault(r => r.Id == round.Id); - } - if (roundToRun != null && nodes != null) - { - roundToRun.Run(nodes); + roundToRun = ActiveRounds.First(r => r.Id == round.Id); + if (roundToRun != null && nodes != null) + { + roundToRun.Run(nodes); + } } } - public static int? GetRoundResult(Round round) + public int? GetRoundResult(Round round) { ActiveRound? roundToRun = null; lock (lockObject) { - roundToRun = ActiveRounds.FirstOrDefault(r => r.Id == round.Id); + roundToRun = ActiveRounds.First(r => r.Id == round.Id); } if (roundToRun != null) { @@ -148,36 +80,29 @@ public static void RunRound(Round round, List? nodes) } } - public static int? FinishRound(Round round, List? nodes) + public int? FinishRound(Round round, List? nodes) { int? result = GetRoundResult(round); RemoveRound(round, nodes); return result; } - public static void CancelRound(Round round, List? nodes) + public void CancelRound(Round round, List? nodes) { RemoveRound(round, nodes); } - internal static void RemoveRound(Round round, List? nodes) + internal void RemoveRound(Round round, List? nodes) { ActiveRound? roundToRemove = null; lock (lockObject) { - roundToRemove = ActiveRounds.FirstOrDefault(r => r.Id == round.Id); + roundToRemove = ActiveRounds.First(r => r.Id == round.Id); if (roundToRemove != null) { + roundToRemove.Clear(nodes); ActiveRounds.Remove(roundToRemove); } } - if (roundToRemove != null) - { - if (roundToRemove.DkgNodes == null && nodes != null) - { - roundToRemove.Init(nodes); - } - roundToRemove.Clear(); - } } } } diff --git a/dkgServiceNode/appsettings.Development.json b/dkgServiceNode/appsettings.Development.json index ff46b69..57ece2e 100644 --- a/dkgServiceNode/appsettings.Development.json +++ b/dkgServiceNode/appsettings.Development.json @@ -1,7 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Information", + "Default": "Debug", "Microsoft.AspNetCore": "Warning", "Microsoft.EntityFrameworkCore": "Warning" } diff --git a/docker-compose.dcproj b/docker-compose.dcproj index f29a2e0..08e1875 100644 --- a/docker-compose.dcproj +++ b/docker-compose.dcproj @@ -10,9 +10,6 @@ dkgservicenode - - docker-compose.yml - docker-compose.yml diff --git a/docker-compose.override.yml b/docker-compose.override.yml deleted file mode 100644 index ca638f2..0000000 --- a/docker-compose.override.yml +++ /dev/null @@ -1,39 +0,0 @@ -version: '3.4' - -services: - dkgservicenode: - environment: - - ASPNETCORE_ENVIRONMENT=Development - - ASPNETCORE_HTTP_PORTS=8080 - - ASPNETCORE_HTTPS_PORTS=8081 - ports: - - "8080:8080" - - "8081:8081" - volumes: - - ${APPDATA}/Microsoft/UserSecrets:/home/app/.microsoft/usersecrets:ro - - ${APPDATA}/ASP.NET/Https:/home/app/.aspnet/https:ro - depends_on: - - dkgservice_db - - dkgservice_db: - container_name: dkgservice_db - image: postgres:16 - restart: unless-stopped - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=postgres - - POSTGRES_DB=dkgservice -# ports: -# - "15432:5432" - volumes: - - pgdata:/var/lib/postgresql - - adminer: - image: adminer - restart: always - ports: - - 8082:8080 - -volumes: - pgdata: {} -