Tendermint、GRPC 和 C# - 流由 RST_STREAM 终止,错误代码:PROTOCOL_ERROR

Tendermint, GRPC and C# - Stream terminated by RST_STREAM with error code: PROTOCOL_ERROR

我的一个项目中需要 Tendermint,但之前从未使用过它,因此我首先尝试从这里实现一个非常简单的示例:https://docs.tendermint.com/master/tutorials/java.html 但在 C#.NET 5.0).

(下载:Minimal Example

我创建了一个简单的 GRPC 服务,试图尽可能地遵循指南:

Startup.cs:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddGrpc();
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IConfiguration conf)
    {
        if (env.IsDevelopment())
            app.UseDeveloperExceptionPage();

        app.UseRouting();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGrpcService<KVStoreService>();
            endpoints.MapGet("/", async context =>
            {
                await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
            });
        });

        app.UseGlobalHostEnvironemnt(env); // These are for tests and are irrelevant for the problem
        app.UseGlobalConfiguration(conf);
        app.UseGlobalLogger();
        app.UseTendermint(); // This starts tendermint process
    }
}

启动 tendermint 使用基本 Process.Start 调用。我将 .toml 配置文件保留为默认值,是的,文档中存在拼写错误,--proxy_app 标志应使用下划线输入(错误很详细):

public static void ConfigureTendermint()
{
    var tendermint = Process.Start(new ProcessStartInfo
    {
        FileName = @"Tendermint\tendermint.exe",
        Arguments = "init validator --home=Tendermint"
    });
    tendermint?.WaitForExit();

    Process.Start(new ProcessStartInfo
    {
        FileName = @"Tendermint\tendermint.exe",
        Arguments = @"node --abci grpc --proxy_app tcp://127.0.0.1:5020 --home=Tendermint --log_level debug"
    });
}

这是正在处理 .proto 个文件的项目文件,它们都已成功生成并运行:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Grpc" Version="2.39.1" />
    <PackageReference Include="Grpc.AspNetCore" Version="2.39.0" />
    <PackageReference Include="Grpc.Tools" Version="2.39.1">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="LiteDB" Version="5.0.11" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\CommonLib.AspNet\CommonLib.AspNet\CommonLib.AspNet.csproj" />
    <ProjectReference Include="..\..\CommonLib\CommonLib\CommonLib.csproj" />
  </ItemGroup>

  <ItemGroup>
    <Protobuf Include="Source\Protos\gogoproto\gogo.proto" GrpcServices="Server" ProtoRoot="Source/Protos" />
    <Protobuf Include="Source\Protos\tendermint\crypto\keys.proto" GrpcServices="Server" ProtoRoot="Source/Protos" />
    <Protobuf Include="Source\Protos\tendermint\crypto\proof.proto" GrpcServices="Server" ProtoRoot="Source/Protos" />
    <Protobuf Include="Source\Protos\tendermint\types\block.proto" GrpcServices="Server" ProtoRoot="Source/Protos" />
    <Protobuf Include="Source\Protos\tendermint\types\canonical.proto" GrpcServices="Server" ProtoRoot="Source/Protos" />
    <Protobuf Include="Source\Protos\tendermint\types\events.proto" GrpcServices="Server" ProtoRoot="Source/Protos" />
    <Protobuf Include="Source\Protos\tendermint\types\evidence.proto" GrpcServices="Server" ProtoRoot="Source/Protos" />
    <Protobuf Include="Source\Protos\tendermint\types\params.proto" GrpcServices="Server" ProtoRoot="Source/Protos" />
    <Protobuf Include="Source\Protos\tendermint\types\types.proto" GrpcServices="Server" ProtoRoot="Source/Protos" />
    <Protobuf Include="Source\Protos\tendermint\types\validator.proto" GrpcServices="Server" ProtoRoot="Source/Protos" />
    <Protobuf Include="Source\Protos\tendermint\version\types.proto" GrpcServices="Server" ProtoRoot="Source/Protos" />
    <Protobuf Include="Source\Protos\tendermint\abci\types.proto" GrpcServices="Server" ProtoRoot="Source/Protos" />
  </ItemGroup>

  <ItemGroup>
    <Folder Include="Source\Database\" />
  </ItemGroup>

</Project>

这是服务本身,它包含翻译成 C#:

的示例(上面链接)
public class KVStoreService : ABCIApplication.ABCIApplicationBase
{
    private static LiteDatabase _env;
    private static ILiteCollection<KV> _store;

    public LiteDatabase Env => _env ??= new LiteDatabase(WebUtils.Configuration?.GetConnectionString("LiteDb"));
    public ILiteCollection<KV> Store => _store ??= Env.GetCollection<KV>("kvs");

    public override Task<ResponseEcho> Echo(RequestEcho request, ServerCallContext context)
    {
        return Task.FromResult(new ResponseEcho { Message = $"Validator is Running: {DateTime.Now:dd-MM-yyyy HH:mm}" });
    }

    public override Task<ResponseCheckTx> CheckTx(RequestCheckTx request, ServerCallContext context)
    {
        var (code, _) = Validate(request.Tx);
        return Task.FromResult(new ResponseCheckTx { Code = code, GasWanted = 1 });
    }
    
    public override Task<ResponseBeginBlock> BeginBlock(RequestBeginBlock request, ServerCallContext context)
    {
        Env.BeginTrans();
        Store.EnsureIndex(x => x.Key, true);
        return Task.FromResult(new ResponseBeginBlock());
    }
    
    public override Task<ResponseDeliverTx> DeliverTx(RequestDeliverTx request, ServerCallContext context)
    {
        var (code, kv) = Validate(request.Tx);
        if (code == 0)
            Store.Insert(kv);
        return Task.FromResult(new ResponseDeliverTx { Code = code });
    }

    public override Task<ResponseCommit> Commit(RequestCommit request, ServerCallContext context)
    {
        Env.Commit();
        return Task.FromResult(new ResponseCommit { Data = ByteString.CopyFrom(new byte[8]) });
    }

    public override Task<ResponseQuery> Query(RequestQuery request, ServerCallContext context)
    {
        var k = request.Data.ToBase64();
        var v = Store.FindOne(x => x.Key == k)?.Value;
        var resp = new ResponseQuery();
        if (v == null)
            resp.Log = $"There is no value for \"{k}\" key";
        else 
        {
            resp.Log = "KVP:";
            resp.Key = ByteString.FromBase64(k);
            resp.Value = ByteString.FromBase64(v);
        }

        return Task.FromResult(resp);
    }

    private (uint, KV) Validate(ByteString tx) 
    {
        var kv = tx.ToStringUtf8().Split('=').Select(kv => kv.UTF8ToBase64()).ToKV();
        if (kv.Key.IsNullOrWhiteSpace() || kv.Value.IsNullOrWhiteSpace())
            return (1, kv);
        
        var stored = Store.FindOne(x => x.Key == kv.Key)?.Value;
        if (stored != null && stored == kv.Value)
            return (2, kv);

        return (0, kv);
    }
}

现在,如果我为我的服务创建一个简单的客户端并同时启动两个项目,它们都将完美运行:

public class Program
{
    public static async Task Main()
    {
        LoggerUtils.Logger.Log(LogLevel.Info, $@"Log Path: {LoggerUtils.LogPath}");
        using var channel = GrpcChannel.ForAddress("http://localhost:5020");
        var client = new ABCIApplication.ABCIApplicationClient(channel);

        var echo = string.Empty;
        while (echo.IsNullOrWhiteSpace())
        {
            try
            {
                echo = client.Echo(new RequestEcho()).Message;
            }
            catch (Exception ex) when (ex is HttpRequestException or RpcException)
            {
                await Task.Delay(1000);
                LoggerUtils.Logger.Log(LogLevel.Info, "Server not Ready, retrying...");
            }
        }
        
        var beginBlock = await client.BeginBlockAsync(new RequestBeginBlock());
        var deliver = await client.DeliverTxAsync(new RequestDeliverTx { Tx = ByteString.CopyFromUtf8("tendermint=rocks") });
        var commit = await client.CommitAsync(new RequestCommit());
        var checkTx = await client.CheckTxAsync(new RequestCheckTx { Tx = ByteString.CopyFromUtf8("tendermint=rocks") });
        var query = await client.QueryAsync(new RequestQuery { Data = ByteString.CopyFromUtf8("tendermint") });

        System.Console.WriteLine($"Echo Status: {echo}");
        System.Console.WriteLine($"Begin Block Status: {(beginBlock != null ? "Success" : "Failure")}");
        System.Console.WriteLine($"Delivery Status: {deliver.Code switch { 0 => "Success", 1 => "Invalid Data", 2 => "Already Exists", _ => "Failure" }}");
        System.Console.WriteLine($"Commit Status: {(commit.Data.ToByteArray().SequenceEqual(new byte[8]) ? "Success" : "Failure")}");
        System.Console.WriteLine($"CheckTx Status: {checkTx.Code switch { 0 => "Success", 1 => "Invalid Data", 2 => "Already Exists", _ => "Failure" }}");
        System.Console.WriteLine($"Query Status: {query.Log} {query.Key.ToStringUtf8()}{(query.Key == ByteString.Empty || query.Value == ByteString.Empty ? "" : "=")}{query.Value.ToStringUtf8()}");

        System.Console.ReadKey();
    }
}

服务器工作:

客户端工作:

到目前为止一切顺利,但是一旦我将 Tendermint 插入管道(以便客户可以通过它调用我的应用程序),出于某种原因,我得到了这个:

Echo failed - module=abci-client connection=query err="rpc error: code = Internal desc = stream terminated by RST_STREAM with error code: PROTOCOL_ERROR"`

无论我如何启动该过程,它都会发生,并且错误会重复出现。显然,由于Tendermint连接代理app失败,所以也无法调用:

上面发布的错误是我将日志设置为调试时得到的所有错误(包括 GRPC 日志)。

"Logging": {
  "LogLevel": {
    "Default": "Debug",
    "Microsoft": "Debug",
    "Microsoft.Hosting.Lifetime": "Debug",
    "Grpc": "Debug"
  }

类似的线程指出了元数据的问题(例如其中的 \n),但我不知道这与我的特定问题有什么关系,您可以去 here 参考.

我很确定这是某个地方的简单配置错误导致 Tendermint 无法与实际应用程序通信的情况,但我无法弄清楚。任何帮助将不胜感激。

[@artur's] 的评论让我开始思考,我终于想通了。实际上,甚至在我发布这个问题之前,我的第一个想法就是这确实应该是 http,尽管文档中另有说明,但不,http://127.0.0.1:5020 是行不通的。所以我试着把它放在 .toml 文件中,我什至尝试过 https,但也没有运气。尝试使用 http 没有抛出任何错误,这与地址前面带有 tcp 的情况不同,它只是挂在 Waiting for Echo 消息上(类似于指向错误地址时,这很奇怪)。我一直,最终恢复到 tcp 版本。解决方案很简单,完全删除协议...

文档没有提供任何线索,所以为了完成,至少在使用 C# (.NET 5) 时,你必须做 3 件事才能让它工作,所有这些都是微不足道的,但你必须先自己弄清楚:

  1. 在指向代理应用程序时从您的配置中删除协议:tcp://127.0.0.1:<port> 应该是 127.0.0.1:<port> 并且是,无论您是否在 .toml 文件中指定协议或作为控制台中的标志。
  2. 标志是 --proxy_app 不是 --proxy-app
  3. 除了遵循本教程之外,您还必须显式覆盖并实现 Info()Echo()InitChain(),否则它会抛出 Unimplemented Exception.

由于我对Tendermint的理解还很匮乏,最初的方法存在一些设计问题。以下是任何面临类似问题的人的代码:

https://github.com/rvnlord/TendermintAbciGrpcCSharp