Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
61286b6
plugin: Add SignClient
Wi1l-B0t Apr 13, 2025
6966048
Merge branch 'master' into plugin.sign-client
cschuchardt88 Apr 15, 2025
60b5fbd
Merge branch 'master' into plugin.sign-client
NGDAdmin Apr 16, 2025
d6a4d2b
Merge branch 'master' into plugin.sign-client
NGDAdmin Apr 17, 2025
517503e
Update src/Plugins/SignClient/SignClient.cs
shargon Apr 17, 2025
f82b80f
Merge branch 'master' into plugin.sign-client
Apr 17, 2025
3337619
optimize: better error message when rpc exception
Wi1l-B0t Apr 17, 2025
9b39bec
Merge branch 'master' into plugin.sign-client
Wi1l-B0t Apr 17, 2025
25ad27e
Merge branch 'master' into plugin.sign-client
Wi1l-B0t Apr 17, 2025
3a6b8d1
Merge branch 'master' into plugin.sign-client
Wi1l-B0t Apr 17, 2025
f119221
optimize: exception message for invalid input
Wi1l-B0t Apr 17, 2025
a978d57
Merge branch 'master' into plugin.sign-client
Wi1l-B0t Apr 18, 2025
298a9de
rename SignerFactory to SignerManger, and add more comments
Wi1l-B0t Apr 19, 2025
359a252
Merge branch 'master' into plugin.sign-client
Apr 21, 2025
52f349e
Merge branch 'master' into plugin.sign-client
NGDAdmin Apr 22, 2025
5bc2526
Merge branch 'master' into plugin.sign-client
Apr 22, 2025
e8383f8
Merge branch 'master' into plugin.sign-client
Wi1l-B0t Apr 24, 2025
66f3033
Merge branch 'master' into plugin.sign-client
Apr 26, 2025
f7f2a16
Merge branch 'master' into plugin.sign-client
NGDAdmin Apr 29, 2025
1aec52b
Merge branch 'master' into plugin.sign-client
Wi1l-B0t Apr 30, 2025
3bc0e65
optimize sign client settings
Wi1l-B0t May 2, 2025
d8d884a
Move EndPoint to settings
shargon May 5, 2025
2258eca
Clean
shargon May 5, 2025
e3d83d4
Merge branch 'master' into plugin.sign-client
shargon May 5, 2025
ad723b5
Merge branch 'master' into plugin.sign-client
Wi1l-B0t May 5, 2025
d6f477b
Merge branch 'master' into plugin.sign-client
cschuchardt88 May 7, 2025
ad78c74
Merge branch 'master' into plugin.sign-client
shargon May 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
349 changes: 343 additions & 6 deletions neo.sln

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/Neo/Sign/SignException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class SignException : Exception
/// Initializes a new instance of the <see cref="SignException"/> class.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public SignException(string message) : base(message) { }
/// <param name="cause">The cause of the exception.</param>
public SignException(string message, Exception cause = null) : base(message, cause) { }
}
}
59 changes: 59 additions & 0 deletions src/Plugins/SignClient/Settings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (C) 2015-2025 The Neo Project.
//
// Settings.cs file belongs to the neo project and is free
// software distributed under the MIT software license, see the
// accompanying file LICENSE in the main directory of the
// repository or http://www.opensource.org/licenses/mit-license.php
// for more details.
//
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

using Microsoft.Extensions.Configuration;

namespace Neo.Plugins.SignClient
{
public class Settings : PluginSettings
{
public const int DefaultPort = 9991;
public const string SectionName = "PluginConfiguration";

/// <summary>
/// The name of the sign client(i.e. Signer).
/// </summary>
public readonly string Name;

/// <summary>
/// The host of the sign client(i.e. Signer).
/// Only support local host at present, so host always is "127.0.0.1" or "::1" now.
/// </summary>
public readonly string Host = "127.0.0.1";

/// <summary>
/// The port of the sign client(i.e. Signer).
/// </summary>
public readonly int Port;

public Settings(IConfigurationSection section) : base(section)
{
Name = section.GetValue("Name", "SignClient");
Port = section.GetValue("Port", DefaultPort);
}

public static Settings Default
{
get
{
var section = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
[SectionName + ":Name"] = "SignClient",
[SectionName + ":Port"] = DefaultPort.ToString()
})
.Build()
.GetSection(SectionName);
return new Settings(section);
}
}
}
}
266 changes: 266 additions & 0 deletions src/Plugins/SignClient/SignClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
// Copyright (C) 2015-2025 The Neo Project.
//
// SignClient.cs file belongs to the neo project and is free
// software distributed under the MIT software license, see the
// accompanying file LICENSE in the main directory of the
// repository or http://www.opensource.org/licenses/mit-license.php
// for more details.
//
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

using Google.Protobuf;
using Grpc.Core;
using Grpc.Net.Client;
using Grpc.Net.Client.Configuration;
using Neo.ConsoleService;
using Neo.Cryptography.ECC;
using Neo.Extensions;
using Neo.Network.P2P;
using Neo.Sign;
using Neo.SmartContract;
using Servicepb;
using Signpb;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using static Neo.SmartContract.Helper;


namespace Neo.Plugins.SignClient
{
/// <summary>
/// A signer that uses a client to sign transactions.
/// </summary>
public class SignClient : Plugin, ISigner
{
private GrpcChannel? _channel;

private SecureSign.SecureSignClient? _client;

private string _name = string.Empty;

public override string Description => "Signer plugin for signer service.";

public override string ConfigFile => System.IO.Path.Combine(RootPath, "SignClient.json");

public SignClient() { }

public SignClient(Settings settings)
{
Reset(settings);
}

// It's for test now.
internal SignClient(string name, SecureSign.SecureSignClient client)
{
Reset(name, client);
}

private void Reset(string name, SecureSign.SecureSignClient? client)
{
if (_client is not null) SignerFactory.UnregisterSigner(_name);

_name = name;
_client = client;
if (!string.IsNullOrEmpty(_name)) SignerFactory.RegisterSigner(_name, this);
}

private void Reset(Settings settings)
{
// _settings = settings;
var methodConfig = new MethodConfig
{
Names = { MethodName.Default },
RetryPolicy = new RetryPolicy
{
MaxAttempts = 3,
InitialBackoff = TimeSpan.FromMilliseconds(50),
MaxBackoff = TimeSpan.FromMilliseconds(200),
BackoffMultiplier = 1.5,
RetryableStatusCodes = {
StatusCode.Cancelled,
StatusCode.DeadlineExceeded,
StatusCode.ResourceExhausted,
StatusCode.Unavailable,
StatusCode.Aborted,
StatusCode.Internal,
StatusCode.DataLoss,
StatusCode.Unknown
}
}
};

// sign server run on localhost, so http is ok
var address = new IPEndPoint(IPAddress.Parse(settings.Host), settings.Port);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to move the parse logic to settings, we will get a possible error earlier

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to move the parse logic to settings, we will get a possible error earlier

Fixed

var channel = GrpcChannel.ForAddress($"http://{address}", new GrpcChannelOptions
{
ServiceConfig = new ServiceConfig { MethodConfigs = { methodConfig } }
});

_channel?.Dispose();
_channel = channel;
Reset(settings.Name, new SecureSign.SecureSignClient(_channel));
}

/// <summary>
/// Get account status command
/// </summary>
/// <param name="hexPublicKey">The hex public key, compressed or uncompressed</param>
[ConsoleCommand("get account status", Category = "Signer Commands", Description = "Get account status")]
public void AccountStatusCommand(string hexPublicKey)
{
var publicKey = ECPoint.DecodePoint(hexPublicKey.HexToBytes(), ECCurve.Secp256r1);
var status = GetAccountStatus(publicKey);
ConsoleHelper.Info("", $"Account status: {status}");
}

private AccountStatus GetAccountStatus(ECPoint publicKey)
{
if (_client is null) throw new SignException("No signer service is connected");

try
{
var output = _client.GetAccountStatus(new()
{
PublicKey = ByteString.CopyFrom(publicKey.EncodePoint(true))
});
return output.Status;
}
catch (RpcException ex)
{
throw new SignException($"Get account status: {ex.Status}", ex);
}
}

/// <inheritdoc/>
public bool ContainsSignable(ECPoint publicKey)
{
var status = GetAccountStatus(publicKey);
return status == AccountStatus.Single || status == AccountStatus.Multiple;
}

private static bool TryDecodePublicKey(ByteString publicKey, [NotNullWhen(true)] out ECPoint? point)
{
try
{
point = ECPoint.DecodePoint(publicKey.Span, ECCurve.Secp256r1);
}
catch (FormatException) // add log later
{
point = null;
}
return point is not null;
}

internal bool Sign(ContractParametersContext context, IEnumerable<AccountSigns> signs)
{
var succeed = false;
foreach (var (accountSigns, scriptHash) in signs.Zip(context.ScriptHashes))
{
var accountStatus = accountSigns.Status;
if (accountStatus == AccountStatus.NoSuchAccount || accountStatus == AccountStatus.NoPrivateKey)
{
succeed |= context.AddWithScriptHash(scriptHash); // Same as Wallet.Sign(context)
continue;
}

var contract = accountSigns.Contract;
var accountContract = Contract.Create(
contract?.Parameters?.Select(p => (ContractParameterType)p).ToArray() ?? [],
contract?.Script?.ToByteArray() ?? []);
if (accountStatus == AccountStatus.Multiple)
{
if (!IsMultiSigContract(accountContract.Script, out int m, out ECPoint[] publicKeys))
throw new SignException("Sign context: multi-sign account but not multi-sign contract");

foreach (var sign in accountSigns.Signs)
{
if (!TryDecodePublicKey(sign.PublicKey, out var publicKey)) continue;

if (!publicKeys.Contains(publicKey))
throw new SignException($"Sign context: public key {publicKey} not in multi-sign contract");

var ok = context.AddSignature(accountContract, publicKey, sign.Signature.ToByteArray());
if (ok) m--;

succeed |= ok;
if (context.Completed || m <= 0) break;
}
}
else if (accountStatus == AccountStatus.Single)
{
if (accountSigns.Signs is null || accountSigns.Signs.Count != 1)
throw new SignException($"Sign context: single account but {accountSigns.Signs?.Count} signs");

var sign = accountSigns.Signs[0];
if (!TryDecodePublicKey(sign.PublicKey, out var publicKey)) continue;
succeed |= context.AddSignature(accountContract, publicKey, sign.Signature.ToByteArray());
}
}
return succeed;
}

/// <inheritdoc/>
public bool Sign(ContractParametersContext context)
{
if (_client is null) throw new SignException("No signer service is connected");

try
{
var signData = context.Verifiable.GetSignData(context.Network);
var output = _client.SignWithScriptHashes(new()
{
SignData = ByteString.CopyFrom(signData),
ScriptHashes = { context.ScriptHashes.Select(h160 => ByteString.CopyFrom(h160.GetSpan())) }
});

int signCount = output.Signs.Count, hashCount = context.ScriptHashes.Count;
if (signCount != hashCount)
{
throw new SignException($"Sign context: Signs.Count({signCount}) != Hashes.Count({hashCount})");
}
return Sign(context, output.Signs);
}
catch (RpcException ex)
{
throw new SignException($"Sign context: {ex.Status}", ex);
}
}

/// <inheritdoc/>
public ReadOnlyMemory<byte> Sign(byte[] signData, ECPoint publicKey)
{
if (_client is null) throw new SignException("No signer service is connected");

try
{
var output = _client.SignWithPublicKey(new()
{
SignData = ByteString.CopyFrom(signData),
PublicKey = ByteString.CopyFrom(publicKey.EncodePoint(true)),
});
return output.Signature.Memory;
}
catch (RpcException ex)
{
throw new SignException($"Sign with public key: {ex.Status}", ex);
}
}

/// <inheritdoc/>
protected override void Configure()
{
var config = GetConfiguration();
if (config is not null) Reset(new Settings(config));
}

/// <inheritdoc/>
public override void Dispose()
{
Reset(string.Empty, null);
_channel?.Dispose();
base.Dispose();
}

}
}
34 changes: 34 additions & 0 deletions src/Plugins/SignClient/SignClient.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<None Update="SignClient.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Neo.ConsoleService\Neo.ConsoleService.csproj" />
</ItemGroup>

<ItemGroup>
<Protobuf Include="proto/signpb.proto" GrpcServices="Client" />
<Protobuf Include="proto/servicepb.proto" GrpcServices="Client" AdditionalImportDirs="$(ProjectDir)/proto" />

<PackageReference Include="Google.Protobuf" Version="3.30.2" />
<PackageReference Include="Grpc.Net.Client" Version="2.70.0" />
<!-- NOTE: Need install rosetta2 on macOS ARM64 -->
<PackageReference Include="Grpc.Tools" Version="2.70.0" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Neo.Plugins.SignClient.Tests" />
</ItemGroup>

</Project>
6 changes: 6 additions & 0 deletions src/Plugins/SignClient/SignClient.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"PluginConfiguration": {
"Name": "SignClient",
"Port": 9991
}
}
Loading