Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ namespace Nethermind.Blockchain.Test.Consensus
public class ClefSignerTests
{
[Test]
public async Task Sign_SigningHash_RequestHasCorrectParameters()
public void Sign_SigningHash_RequestHasCorrectParameters()
{
IJsonRpcClient client = Substitute.For<IJsonRpcClient>();
client.Post<string[]>("account_list").Returns(Task.FromResult<string[]?>([TestItem.AddressA!.ToString()]));
Task<string?> postMethod = client.Post<string>("account_signData", "text/plain", Arg.Any<string>(), Keccak.Zero);
var returnValue = (new byte[65]).ToHexString();
postMethod.Returns(returnValue);
ClefSigner sut = await ClefSigner.Create(client);
ClefSigner sut = ClefSigner.Create(new ClefWallet(client));

var result = sut.Sign(Keccak.Zero);

Expand All @@ -41,7 +41,7 @@ public async Task Sign_SigningCliqueHeader_PassingCorrectClefParametersForReques
var returnValue = (new byte[65]).ToHexString();
postMethod.Returns(returnValue);
BlockHeader blockHeader = Build.A.BlockHeader.TestObject;
ClefSigner sut = await ClefSigner.Create(client);
ClefSigner sut = ClefSigner.Create(new ClefWallet(client));

sut.Sign(blockHeader);

Expand All @@ -51,7 +51,7 @@ public async Task Sign_SigningCliqueHeader_PassingCorrectClefParametersForReques

[TestCase(0, 27)]
[TestCase(1, 28)]
public async Task Sign_RecoveryIdIsSetToCliqueValues_RecoveryIdIsAdjusted(byte recId, byte expected)
public void Sign_RecoveryIdIsSetToCliqueValues_RecoveryIdIsAdjusted(byte recId, byte expected)
{
IJsonRpcClient client = Substitute.For<IJsonRpcClient>();
client.Post<string[]>("account_list").Returns(Task.FromResult<string[]?>([TestItem.AddressA!.ToString()]));
Expand All @@ -60,20 +60,20 @@ public async Task Sign_RecoveryIdIsSetToCliqueValues_RecoveryIdIsAdjusted(byte r
returnValue[64] = recId;
postMethod.Returns(returnValue.ToHexString());
BlockHeader blockHeader = Build.A.BlockHeader.TestObject;
ClefSigner sut = await ClefSigner.Create(client);
ClefSigner sut = ClefSigner.Create(new ClefWallet(client));

var result = sut.Sign(blockHeader);

Assert.That(result.V, Is.EqualTo(expected));
}

[Test]
public async Task Create_SignerAddressSpecified_CorrectAddressIsSet()
public void Create_SignerAddressSpecified_CorrectAddressIsSet()
{
IJsonRpcClient client = Substitute.For<IJsonRpcClient>();
client.Post<string[]>("account_list").Returns(Task.FromResult<string[]?>([TestItem.AddressA!.ToString(), TestItem.AddressB!.ToString()]));

ClefSigner sut = await ClefSigner.Create(client, TestItem.AddressB);
ClefSigner sut = ClefSigner.Create(new ClefWallet(client), TestItem.AddressB);

Assert.That(sut.Address, Is.EqualTo(TestItem.AddressB));
}
Expand All @@ -84,15 +84,15 @@ public void Create_SignerAddressDoesNotExists_ThrowInvalidOperationException()
IJsonRpcClient client = Substitute.For<IJsonRpcClient>();
client.Post<string[]>("account_list").Returns(Task.FromResult<string[]?>([TestItem.AddressA!.ToString(), TestItem.AddressB!.ToString()]));

Assert.That(async () => await ClefSigner.Create(client, TestItem.AddressC), Throws.InstanceOf<InvalidOperationException>());
Assert.That(() => ClefSigner.Create(new ClefWallet(client), TestItem.AddressC), Throws.InstanceOf<InvalidOperationException>());
}

[Test]
public async Task SetSigner_TryingToASigner_ThrowInvalidOperationException()
public void SetSigner_TryingToASigner_ThrowInvalidOperationException()
{
IJsonRpcClient client = Substitute.For<IJsonRpcClient>();
client.Post<string[]>("account_list").Returns(Task.FromResult<string[]?>([TestItem.AddressA!.ToString()]));
ClefSigner sut = await ClefSigner.Create(client);
ClefSigner sut = ClefSigner.Create(new ClefWallet(client));

Assert.That(() => sut.SetSigner(Build.A.PrivateKey.TestObject), Throws.InstanceOf<InvalidOperationException>());
}
Expand Down
65 changes: 12 additions & 53 deletions src/Nethermind/Nethermind.ExternalSigner.Plugin/ClefSigner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,26 @@

using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using DotNetty.Buffers;
using Nethermind.Consensus;
using Nethermind.Core;
using Nethermind.Core.Crypto;
using Nethermind.Core.Extensions;
using Nethermind.Crypto;
using Nethermind.JsonRpc.Client;
using Nethermind.Serialization.Rlp;

namespace Nethermind.ExternalSigner.Plugin;

public class ClefSigner : IHeaderSigner, ISignerStore
{
private readonly IJsonRpcClient _rpcClient;
private readonly HeaderDecoder _headerDecoder = new();

private ClefSigner(IJsonRpcClient rpcClient, Address author)
private readonly ClefWallet _clefWallet;

private ClefSigner(ClefWallet clefWallet, Address author)
{
_rpcClient = rpcClient;
Address = author;
_clefWallet = clefWallet;
}

public static async Task<ClefSigner> Create(IJsonRpcClient jsonRpcClient, Address? blockAuthorAccount = null) =>
new(jsonRpcClient, await GetSignerAddress(jsonRpcClient, blockAuthorAccount));
public static ClefSigner Create(ClefWallet clefWallet, Address? blockAuthorAccount = null) =>
new(clefWallet, GetSignerAddress(clefWallet, blockAuthorAccount));

public Address Address { get; }

Expand All @@ -44,15 +40,7 @@ public static async Task<ClefSigner> Create(IJsonRpcClient jsonRpcClient, Addres
/// <returns><see cref="Signature"/> of <paramref name="message"/>.</returns>
public Signature Sign(Hash256 message)
{
var signed = _rpcClient.Post<string>(
"account_signData",
"text/plain",
Address.ToString(),
message)
.GetAwaiter().GetResult();
if (signed is null) ThrowInvalidOperationSignFailed();
byte[] bytes = Bytes.FromHexString(signed);
return new Signature(bytes);
return _clefWallet.Sign(message, Address);
}

/// <summary>
Expand All @@ -63,56 +51,27 @@ public Signature Sign(Hash256 message)
/// <returns><see cref="Signature"/> of the hash of the clique header.</returns>
public Signature Sign(BlockHeader header)
{
ArgumentNullException.ThrowIfNull(header);
int contentLength = _headerDecoder.GetLength(header, RlpBehaviors.None);
IByteBuffer buffer = PooledByteBufferAllocator.Default.Buffer(contentLength);
try
{
RlpStream rlpStream = new NettyRlpStream(buffer);
rlpStream.Encode(header);
string? signed = _rpcClient.Post<string>(
"account_signData",
"application/x-clique-header",
Address.ToString(),
buffer.AsSpan().ToHexString(true))
.GetAwaiter().GetResult();
if (signed is null) ThrowInvalidOperationSignFailed();
byte[] bytes = Bytes.FromHexString(signed);

//Clef will set recid to 0/1, without the VOffset
return bytes.Length == 65 && (bytes[64] == 0 || bytes[64] == 1)
? new Signature(bytes.AsSpan(0, 64), bytes[64])
: new Signature(bytes);
}
finally
{
buffer.Release();
}
return _clefWallet.Sign(header, Address);
}

public ValueTask Sign(Transaction tx) =>
throw new NotImplementedException("Remote signing of transactions is not supported.");

private static async Task<Address> GetSignerAddress(IJsonRpcClient rpcClient, Address? blockAuthorAccount)
private static Address GetSignerAddress(ClefWallet clefWallet, Address? blockAuthorAccount)
{
var accounts = await rpcClient.Post<string[]>("account_list") ?? throw new InvalidOperationException("Remote signer 'account_list' response is invalid.");
Address[] accounts = clefWallet.GetAccounts();
if (accounts.Length == 0) throw new InvalidOperationException("Remote signer has not been configured with any signers.");
return blockAuthorAccount is not null
? accounts.Any(a => new Address(a).Bytes.SequenceEqual(blockAuthorAccount.Bytes))
? accounts.Any(a => a == blockAuthorAccount)
? blockAuthorAccount
: throw new InvalidOperationException($"Remote signer cannot sign for {blockAuthorAccount}.")
: new Address(accounts[0]);
: accounts[0];
}

public void SetSigner(PrivateKey key) => ThrowInvalidOperationSetSigner();

public void SetSigner(IProtectedPrivateKey key) => ThrowInvalidOperationSetSigner();

[DoesNotReturn]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ThrowInvalidOperationSignFailed() =>
throw new InvalidOperationException("Remote signer failed to sign the request.");

[DoesNotReturn]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ThrowInvalidOperationSetSigner() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Nethermind.Consensus;
using Nethermind.KeyStore.Config;
using System.Configuration;
using Nethermind.Wallet;

namespace Nethermind.ExternalSigner.Plugin;

Expand All @@ -23,41 +24,50 @@ public class ClefSignerPlugin(IMiningConfig miningConfig) : INethermindPlugin
public string Author => "Nethermind";

public bool MustInitialize => true;
public bool Enabled => miningConfig.Enabled;
public bool Enabled => !string.IsNullOrEmpty(miningConfig.Signer);

public ValueTask DisposeAsync() => ValueTask.CompletedTask;

public async Task Init(INethermindApi nethermindApi)
public Task Init(INethermindApi nethermindApi)
{
_nethermindApi = nethermindApi ?? throw new ArgumentNullException(nameof(nethermindApi));
if (!string.IsNullOrEmpty(miningConfig.Signer))
{
if (!Uri.TryCreate(miningConfig.Signer, UriKind.Absolute, out Uri? uri))
{
throw new ConfigurationErrorsException($"{miningConfig.Signer} must have be a valid uri.");
throw new ConfigurationErrorsException($"{miningConfig.Signer} must be a valid uri.");
}

string blockAuthorAccount = _nethermindApi.Config<IKeyStoreConfig>().BlockAuthorAccount;
_nethermindApi.EngineSigner = await SetupExternalSigner(uri, blockAuthorAccount);

BasicJsonRpcClient rpcClient = new(uri, _nethermindApi!.EthereumJsonSerializer, _nethermindApi.LogManager, TimeSpan.FromSeconds(10));
_nethermindApi.DisposeStack.Push(rpcClient);

ClefWallet clefWallet = new(rpcClient);
_nethermindApi.Wallet = clefWallet;

if (miningConfig.Enabled)
_nethermindApi.EngineSigner = SetupExternalSigner(clefWallet, blockAuthorAccount);

}
return Task.CompletedTask;
}

public Task InitNetworkProtocol() => Task.CompletedTask;

public Task InitRpcModules() => Task.CompletedTask;

private async Task<ClefSigner> SetupExternalSigner(Uri urlSigner, string blockAuthorAccount)
private ClefSigner SetupExternalSigner(ClefWallet clefWallet, string blockAuthorAccount)
{
try
{
Address? address = string.IsNullOrEmpty(blockAuthorAccount) ? null : new Address(blockAuthorAccount);
BasicJsonRpcClient rpcClient = new(urlSigner, _nethermindApi!.EthereumJsonSerializer, _nethermindApi.LogManager, TimeSpan.FromSeconds(10));
_nethermindApi.DisposeStack.Push(rpcClient);
return await ClefSigner.Create(rpcClient, address);

return ClefSigner.Create(clefWallet, address);
}
catch (HttpRequestException e)
{
throw new NetworkingException($"Remote signer at {urlSigner} did not respond.", NetworkExceptionType.TargetUnreachable, e);
throw new NetworkingException($"Remote signer did not respond during setup.", NetworkExceptionType.TargetUnreachable, e);
}
}
}
Loading