Skip to content
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

Shutter: validator registry V1 #7682

Merged
merged 8 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions src/Nethermind/Nethermind.Crypto/BlsSigner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public static Signature Sign(Span<long> buf, Bls.SecretKey sk, ReadOnlySpan<byte
public static Signature Sign(Bls.SecretKey sk, ReadOnlySpan<byte> message)
=> Sign(new long[G2.Sz], sk, message);

[SkipLocalsInit]
public static bool Verify(G1Affine publicKey, Signature signature, ReadOnlySpan<byte> message)
{
int len = 2 * GT.Sz;
Expand Down Expand Up @@ -126,6 +127,9 @@ public AggregatedPublicKey(Span<long> buf)
public void FromSk(Bls.SecretKey sk)
=> _point.FromSk(sk);

public void Reset()
=> _point.Zero();

public bool TryDecode(ReadOnlySpan<byte> publicKeyBytes, out Bls.ERROR err)
=> _point.TryDecode(publicKeyBytes, out err);

Expand All @@ -138,6 +142,7 @@ public void Aggregate(G1Affine publicKey)
public void Aggregate(AggregatedPublicKey aggregatedPublicKey)
=> _point.Aggregate(aggregatedPublicKey.PublicKey);

[SkipLocalsInit]
public bool TryAggregate(ReadOnlySpan<byte> publicKeyBytes, out Bls.ERROR err)
{
G1Affine pk = new(stackalloc long[G1Affine.Sz]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,4 @@ public void Ignores_outdated_block()
api.TriggerNewHeadBlock(new(Build.A.Block.WithTimestamp(upToDateTimestamp).TestObject));
Assert.That(api.EonUpdateCalled, Is.EqualTo(1));
}

}
9 changes: 4 additions & 5 deletions src/Nethermind/Nethermind.Shutter.Test/ShutterCryptoTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,10 @@ public void Can_decrypt_data(string cipherTextHex, string decryptionKeyHex, stri
)]
public void Can_verify_validator_registration_signature(string msgHex, string sigHex, string pkHex)
{
Assert.That(ShutterCrypto.CheckValidatorRegistrySignature(
new(Convert.FromHexString(pkHex)),
Convert.FromHexString(sigHex),
Convert.FromHexString(msgHex)
));
BlsSigner.AggregatedPublicKey pk = new();
pk.Decode(Convert.FromHexString(pkHex));

Assert.That(ShutterCrypto.CheckValidatorRegistrySignatures(pk, Convert.FromHexString(sigHex), Convert.FromHexString(msgHex)));
}

[Test]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using NUnit.Framework;
using Nethermind.Core;
using System;
using Nethermind.Shutter.Contracts;
using NSubstitute;
using Nethermind.Evm.TransactionProcessing;
using Nethermind.Logging;
using System.Collections.Generic;
using Nethermind.Shutter.Config;
using Nethermind.Crypto;
using Nethermind.Core.Crypto;

using Update = (byte[] Message, byte[] Signature);
using G1 = Nethermind.Crypto.Bls.P1;

namespace Nethermind.Shutter.Test;

[TestFixture]
class ShutterValidatorRegistryTests
{
private static readonly byte[] SkBytes = [0x2c, 0xd4, 0xba, 0x40, 0x6b, 0x52, 0x24, 0x59, 0xd5, 0x7a, 0x0b, 0xed, 0x51, 0xa3, 0x97, 0x43, 0x5c, 0x0b, 0xb1, 0x1d, 0xd5, 0xf3, 0xca, 0x11, 0x52, 0xb3, 0x69, 0x4b, 0xb9, 0x1d, 0x7c, 0x22];

[Test]
public void Can_check_if_registered()
{
ValidatorRegistryContract contract = new(
Substitute.For<ITransactionProcessor>(),
ShutterTestsCommon.AbiEncoder,
Address.Zero,
LimboLogs.Instance,
ShutterTestsCommon.ChainId,
1);
ShutterValidatorsInfo validatorsInfo = new();
List<(uint, Update)> updates = [];

// populate validatorsInfo
G1 pk = new();
for (ulong i = 100; i < 110; i++)
{
Bls.SecretKey sk = GetSecretKeyForIndex((uint)i);
pk.FromSk(sk);
validatorsInfo.Add(i, pk.ToAffine().Point.ToArray());
}

// register all 10, then deregister last 5
updates.Add((0, CreateUpdate(100, 10, 0, true)));
updates.Add((1, CreateUpdate(105, 5, 1, false)));

// invalid updates should be ignored
updates.Add((2, CreateUpdate(100, 10, 0, false))); // invalid nonce
updates.Add((3, CreateUpdate(50, 50, 0, true))); // not in validatorsInfo

// bad signature
Update badUpdate = CreateUpdate(100, 10, 2, true);
badUpdate.Signature[34] += 1;
updates.Add((4, badUpdate));

Assert.Multiple(() =>
{
Assert.That(!contract.IsRegistered(updates, validatorsInfo, out HashSet<ulong> unregistered));
Assert.That(unregistered, Has.Count.EqualTo(5));
});
}

private static Update CreateUpdate(ulong startIndex, uint count, uint nonce, bool isRegistration)
{
ValidatorRegistryContract.Message msg = new()
{
Version = 1,
ChainId = ShutterTestsCommon.ChainId,
ContractAddress = Address.Zero.Bytes,
StartValidatorIndex = startIndex,
Count = count,
Nonce = nonce,
IsRegistration = isRegistration
};
byte[] msgBytes = msg.Encode();
ReadOnlySpan<byte> msgHash = ValueKeccak.Compute(msgBytes).Bytes;

BlsSigner.Signature agg = new();
BlsSigner.Signature s = new();

ulong endIndex = startIndex + count;
for (ulong i = startIndex; i < endIndex; i++)
{
Bls.SecretKey sk = GetSecretKeyForIndex((uint)i);
s.Sign(sk, msgHash);
agg.Aggregate(s);
}

return (msgBytes, agg.Bytes.ToArray());
}

private static Bls.SecretKey GetSecretKeyForIndex(uint index)
{
// n.b. doesn't have to derive from master key, just done for convenience
Bls.SecretKey masterSk = new(SkBytes, Bls.ByteOrder.LittleEndian);
return new(masterSk, index);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public interface IShutterConfig : IConfig
string ShutterKeyFile { get; set; }

[ConfigItem(Description = "The Shutter validator registry message version.",
DefaultValue = "0", HiddenFromDocs = true)]
DefaultValue = "1", HiddenFromDocs = true)]
ulong ValidatorRegistryMessageVersion { get; set; }

[ConfigItem(Description = "The maximum amount of gas to use on Shutter transactions.",
Expand Down
2 changes: 1 addition & 1 deletion src/Nethermind/Nethermind.Shutter/Config/ShutterConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class ShutterConfig : IShutterConfig
public string? P2PProtocolVersion { get; set; } = "/shutter/0.1.0";
public string? P2PAgentVersion { get; set; } = "github.com/shutter-network/rolling-shutter/rolling-shutter";
public string ShutterKeyFile { get; set; } = "shutter.key.plain";
public ulong ValidatorRegistryMessageVersion { get; set; } = 0;
public ulong ValidatorRegistryMessageVersion { get; set; } = 1;
public ulong InstanceID { get; set; } = 0;
public int EncryptedGasLimit { get; set; } = 10000000;
public ushort MaxKeyDelay { get; set; } = 1666;
Expand Down
44 changes: 28 additions & 16 deletions src/Nethermind/Nethermind.Shutter/Config/ShutterValidatorsInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,56 @@
using System.IO;
using Nethermind.Crypto;
using Nethermind.Serialization.Json;

using G1Affine = Nethermind.Crypto.Bls.P1Affine;

namespace Nethermind.Shutter.Config;

public class ShutterValidatorsInfo
{
public bool IsEmpty { get => _indexToPubKeyBytes is null || _indexToPubKeyBytes.Count == 0; }
public IEnumerable<ulong> ValidatorIndices { get => _indexToPubKeyBytes!.Keys; }
public bool IsEmpty { get => _indexToPubKey is null || _indexToPubKey.Count == 0; }
public IEnumerable<ulong> ValidatorIndices { get => _indexToPubKey!.Keys; }
public class ShutterValidatorsInfoException(string message) : Exception(message);

private Dictionary<ulong, byte[]>? _indexToPubKeyBytes;
private readonly Dictionary<ulong, long[]> _indexToPubKey = [];
protected readonly Dictionary<ulong, long[]> _indexToPubKey = [];
protected ulong _minIndex = ulong.MaxValue;
protected ulong _maxIndex = ulong.MinValue;

public void Load(string fp)
{
FileStream fstream = new(fp, FileMode.Open, FileAccess.Read, FileShare.None);
_indexToPubKeyBytes = new EthereumJsonSerializer().Deserialize<Dictionary<ulong, byte[]>>(fstream);
FileStream fstream = new(fp, FileMode.Open, FileAccess.Read, FileShare.Read);
Dictionary<ulong, byte[]> indexToPubKeyBytes = new EthereumJsonSerializer().Deserialize<Dictionary<ulong, byte[]>>(fstream);
AddPublicKeys(indexToPubKeyBytes);
}

public void Validate()
public bool ContainsIndex(ulong index)
=> _indexToPubKey!.ContainsKey(index);

// non inclusive of end index
public bool MayContainIndexInRange(ulong startIndex, ulong endIndex)
=> (endIndex <= _maxIndex && endIndex > _minIndex) || (startIndex < _maxIndex && startIndex >= _minIndex);

public G1Affine GetPubKey(ulong index)
=> new(_indexToPubKey[index]);

internal void Add(ulong index, long[] pubkey)
{
_indexToPubKey.Add(index, pubkey);
_minIndex = Math.Min(_minIndex, index);
_maxIndex = Math.Max(_maxIndex, index + 1);
}

private void AddPublicKeys(Dictionary<ulong, byte[]> indexToPubKeyBytes)
{
G1Affine pk = new(stackalloc long[G1Affine.Sz]);

foreach ((ulong index, byte[] pubkey) in _indexToPubKeyBytes!)
foreach ((ulong index, byte[] pubkey) in indexToPubKeyBytes)
{
if (!pk.TryDecode(pubkey, out Bls.ERROR _))
{
throw new ShutterValidatorsInfoException($"Validator info file contains invalid public key with index {index}.");
}

_indexToPubKey.Add(index, pk.Point.ToArray());
Add(index, pk.Point.ToArray());
}
}

public bool IsIndexRegistered(ulong index)
=> _indexToPubKeyBytes!.ContainsKey(index);

public G1Affine GetPubKey(ulong index)
=> new(_indexToPubKey[index]);
}
Loading