-
Notifications
You must be signed in to change notification settings - Fork 1k
[Add]: Add SignClient Plugin
#3896
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
+1,093
−15
Closed
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 6966048
Merge branch 'master' into plugin.sign-client
cschuchardt88 60b5fbd
Merge branch 'master' into plugin.sign-client
NGDAdmin d6a4d2b
Merge branch 'master' into plugin.sign-client
NGDAdmin 517503e
Update src/Plugins/SignClient/SignClient.cs
shargon f82b80f
Merge branch 'master' into plugin.sign-client
3337619
optimize: better error message when rpc exception
Wi1l-B0t 9b39bec
Merge branch 'master' into plugin.sign-client
Wi1l-B0t 25ad27e
Merge branch 'master' into plugin.sign-client
Wi1l-B0t 3a6b8d1
Merge branch 'master' into plugin.sign-client
Wi1l-B0t f119221
optimize: exception message for invalid input
Wi1l-B0t a978d57
Merge branch 'master' into plugin.sign-client
Wi1l-B0t 298a9de
rename SignerFactory to SignerManger, and add more comments
Wi1l-B0t 359a252
Merge branch 'master' into plugin.sign-client
52f349e
Merge branch 'master' into plugin.sign-client
NGDAdmin 5bc2526
Merge branch 'master' into plugin.sign-client
e8383f8
Merge branch 'master' into plugin.sign-client
Wi1l-B0t 66f3033
Merge branch 'master' into plugin.sign-client
f7f2a16
Merge branch 'master' into plugin.sign-client
NGDAdmin 1aec52b
Merge branch 'master' into plugin.sign-client
Wi1l-B0t 3bc0e65
optimize sign client settings
Wi1l-B0t d8d884a
Move EndPoint to settings
shargon 2258eca
Clean
shargon e3d83d4
Merge branch 'master' into plugin.sign-client
shargon ad723b5
Merge branch 'master' into plugin.sign-client
Wi1l-B0t d6f477b
Merge branch 'master' into plugin.sign-client
cschuchardt88 ad78c74
Merge branch 'master' into plugin.sign-client
shargon File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| var channel = GrpcChannel.ForAddress($"http://{address}", new GrpcChannelOptions | ||
shargon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| 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(); | ||
| } | ||
|
|
||
shargon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "PluginConfiguration": { | ||
| "Name": "SignClient", | ||
| "Port": 9991 | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed