From 5af8f17f8ccf08863a6d6d508a0fd365e76fca4f Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 1 Nov 2024 18:03:24 +0000 Subject: [PATCH] Shutter: validator registry V1 (#7682) --- src/Nethermind/Nethermind.Crypto/BlsSigner.cs | 5 + .../ShutterBlockHandlerTests.cs | 1 - .../ShutterCryptoTests.cs | 9 +- .../ShutterValidatorRegistryTests.cs | 103 +++++++++++ .../Config/IShutterConfig.cs | 2 +- .../Config/ShutterConfig.cs | 2 +- .../Config/ShutterValidatorsInfo.cs | 44 +++-- .../Contracts/ValidatorRegistryContract.cs | 165 ++++++++++++------ .../Nethermind.Shutter/ShutterCrypto.cs | 5 +- .../Nethermind.Shutter/ShutterPlugin.cs | 1 - 10 files changed, 260 insertions(+), 77 deletions(-) create mode 100644 src/Nethermind/Nethermind.Shutter.Test/ShutterValidatorRegistryTests.cs diff --git a/src/Nethermind/Nethermind.Crypto/BlsSigner.cs b/src/Nethermind/Nethermind.Crypto/BlsSigner.cs index 93f9e95aeec..f8a5fcf35d4 100644 --- a/src/Nethermind/Nethermind.Crypto/BlsSigner.cs +++ b/src/Nethermind/Nethermind.Crypto/BlsSigner.cs @@ -40,6 +40,7 @@ public static Signature Sign(Span buf, Bls.SecretKey sk, ReadOnlySpan message) => Sign(new long[G2.Sz], sk, message); + [SkipLocalsInit] public static bool Verify(G1Affine publicKey, Signature signature, ReadOnlySpan message) { int len = 2 * GT.Sz; @@ -126,6 +127,9 @@ public AggregatedPublicKey(Span buf) public void FromSk(Bls.SecretKey sk) => _point.FromSk(sk); + public void Reset() + => _point.Zero(); + public bool TryDecode(ReadOnlySpan publicKeyBytes, out Bls.ERROR err) => _point.TryDecode(publicKeyBytes, out err); @@ -138,6 +142,7 @@ public void Aggregate(G1Affine publicKey) public void Aggregate(AggregatedPublicKey aggregatedPublicKey) => _point.Aggregate(aggregatedPublicKey.PublicKey); + [SkipLocalsInit] public bool TryAggregate(ReadOnlySpan publicKeyBytes, out Bls.ERROR err) { G1Affine pk = new(stackalloc long[G1Affine.Sz]); diff --git a/src/Nethermind/Nethermind.Shutter.Test/ShutterBlockHandlerTests.cs b/src/Nethermind/Nethermind.Shutter.Test/ShutterBlockHandlerTests.cs index f1224ebcb42..75c148310a9 100644 --- a/src/Nethermind/Nethermind.Shutter.Test/ShutterBlockHandlerTests.cs +++ b/src/Nethermind/Nethermind.Shutter.Test/ShutterBlockHandlerTests.cs @@ -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)); } - } diff --git a/src/Nethermind/Nethermind.Shutter.Test/ShutterCryptoTests.cs b/src/Nethermind/Nethermind.Shutter.Test/ShutterCryptoTests.cs index 4122084bf22..0bf7a81154f 100644 --- a/src/Nethermind/Nethermind.Shutter.Test/ShutterCryptoTests.cs +++ b/src/Nethermind/Nethermind.Shutter.Test/ShutterCryptoTests.cs @@ -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] diff --git a/src/Nethermind/Nethermind.Shutter.Test/ShutterValidatorRegistryTests.cs b/src/Nethermind/Nethermind.Shutter.Test/ShutterValidatorRegistryTests.cs new file mode 100644 index 00000000000..0a41c7bbd03 --- /dev/null +++ b/src/Nethermind/Nethermind.Shutter.Test/ShutterValidatorRegistryTests.cs @@ -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(), + 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 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 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); + } +} diff --git a/src/Nethermind/Nethermind.Shutter/Config/IShutterConfig.cs b/src/Nethermind/Nethermind.Shutter/Config/IShutterConfig.cs index f96b1b8c0dc..d074ccf4901 100644 --- a/src/Nethermind/Nethermind.Shutter/Config/IShutterConfig.cs +++ b/src/Nethermind/Nethermind.Shutter/Config/IShutterConfig.cs @@ -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.", diff --git a/src/Nethermind/Nethermind.Shutter/Config/ShutterConfig.cs b/src/Nethermind/Nethermind.Shutter/Config/ShutterConfig.cs index a8ed781e0a8..d68d26b11e6 100644 --- a/src/Nethermind/Nethermind.Shutter/Config/ShutterConfig.cs +++ b/src/Nethermind/Nethermind.Shutter/Config/ShutterConfig.cs @@ -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; diff --git a/src/Nethermind/Nethermind.Shutter/Config/ShutterValidatorsInfo.cs b/src/Nethermind/Nethermind.Shutter/Config/ShutterValidatorsInfo.cs index 98211c92e07..63d01db8421 100644 --- a/src/Nethermind/Nethermind.Shutter/Config/ShutterValidatorsInfo.cs +++ b/src/Nethermind/Nethermind.Shutter/Config/ShutterValidatorsInfo.cs @@ -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 ValidatorIndices { get => _indexToPubKeyBytes!.Keys; } + public bool IsEmpty { get => _indexToPubKey is null || _indexToPubKey.Count == 0; } + public IEnumerable ValidatorIndices { get => _indexToPubKey!.Keys; } public class ShutterValidatorsInfoException(string message) : Exception(message); - private Dictionary? _indexToPubKeyBytes; - private readonly Dictionary _indexToPubKey = []; + protected readonly Dictionary _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>(fstream); + FileStream fstream = new(fp, FileMode.Open, FileAccess.Read, FileShare.Read); + Dictionary indexToPubKeyBytes = new EthereumJsonSerializer().Deserialize>(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 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]); } diff --git a/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs b/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs index 9270a90ba32..1c72f1ea222 100644 --- a/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs +++ b/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs @@ -11,9 +11,11 @@ using Nethermind.Logging; using System.Collections.Generic; using Nethermind.Core.Extensions; -using Update = (byte[] Message, byte[] Signature); using Nethermind.Crypto; using Nethermind.Shutter.Config; +using System.Linq; + +using Update = (byte[] Message, byte[] Signature); namespace Nethermind.Shutter.Contracts; @@ -34,6 +36,9 @@ public Update GetUpdate(BlockHeader header, in UInt256 i) => (Update)Call(header, nameof(GetUpdate), Address.Zero, [i])[0]; public bool IsRegistered(in BlockHeader header, in ShutterValidatorsInfo validatorsInfo, out HashSet unregistered) + => IsRegistered(GetUpdates(header), validatorsInfo, out unregistered); + + internal bool IsRegistered(IEnumerable<(uint, Update)> updates, in ShutterValidatorsInfo validatorsInfo, out HashSet unregistered) { Dictionary nonces = []; unregistered = []; @@ -43,80 +48,128 @@ public bool IsRegistered(in BlockHeader header, in ShutterValidatorsInfo validat unregistered.Add(index); } - uint updates = (uint)GetNumUpdates(header); - for (uint i = 0; i < updates; i++) + foreach ((uint i, Update update) in updates) { - Update update = GetUpdate(header, updates - i - 1); - - if (update.Message.Length != Message.Sz || update.Signature.Length != BlsSigner.Signature.Sz) + if (!IsUpdateValid(update, validatorsInfo, out string err)) { - if (_logger.IsDebug) _logger.Debug("Registration message was wrong length."); + if (_logger.IsDebug) _logger.Debug($"Update {i} was invalid: {err}"); continue; } Message msg = new(update.Message.AsSpan()); + UpdateRegistrations(msg, nonces, unregistered); + } - // skip untracked validators - if (!validatorsInfo.IsIndexRegistered(msg.ValidatorIndex)) - { - continue; - } + return unregistered.Count == 0; + } - if (msg.Version != messageVersion) - { - if (_logger.IsDebug) _logger.Debug($"Registration message has wrong version ({msg.Version}) should be {messageVersion}"); - continue; - } + private IEnumerable<(uint, Update)> GetUpdates(BlockHeader header) + { + uint updates = (uint)GetNumUpdates(header); + for (uint i = 0; i < updates; i++) + { + yield return (i, GetUpdate(header, updates - i - 1)); + } + } - if (msg.ChainId != chainId) + private void UpdateRegistrations(Message msg, Dictionary nonces, HashSet unregistered) + { + ulong endValidatorIndex = msg.StartValidatorIndex + msg.Count; + for (ulong v = msg.StartValidatorIndex; v < endValidatorIndex; v++) + { + if (nonces[v].HasValue && msg.Nonce <= nonces[v]) { - if (_logger.IsDebug) _logger.Debug($"Registration message has incorrect chain ID ({msg.ChainId}) should be {chainId}"); + if (_logger.IsDebug) _logger.Debug($"Registration message for validator index {v} has incorrect nonce ({msg.Nonce}) should be {nonces[v] + 1}"); continue; } - if (!msg.ContractAddress.SequenceEqual(ContractAddress!.Bytes)) - { - if (_logger.IsDebug) _logger.Debug($"Registration message contains an invalid contract address ({msg.ContractAddress.ToHexString()}) should be {ContractAddress}"); - continue; - } + nonces[v] = msg.Nonce; - if (nonces[msg.ValidatorIndex].HasValue && msg.Nonce <= nonces[msg.ValidatorIndex]) + if (msg.IsRegistration) { - if (_logger.IsDebug) _logger.Debug($"Registration message has incorrect nonce ({msg.Nonce}) should be {nonces[msg.ValidatorIndex]}"); - continue; + unregistered.Remove(v); } - - if (!ShutterCrypto.CheckValidatorRegistrySignature(validatorsInfo.GetPubKey(msg.ValidatorIndex), update.Signature, update.Message)) + else { - if (_logger.IsDebug) _logger.Debug("Registration message has invalid signature."); - continue; + unregistered.Add(v); } + } + } - // message is valid - nonces[msg.ValidatorIndex] = msg.Nonce; + private bool IsUpdateValid(in Update update, in ShutterValidatorsInfo validatorsInfo, out string err) + { + if (update.Message.Length != Message.Sz || update.Signature.Length != BlsSigner.Signature.Sz) + { + err = "Registration message was wrong length."; + return false; + } - if (msg.IsRegistration) - { - unregistered.Remove(msg.ValidatorIndex); - } - else + Message msg = new(update.Message.AsSpan()); + ulong startValidatorIndex = msg.StartValidatorIndex; + ulong endValidatorIndex = msg.StartValidatorIndex + msg.Count; + + // skip validator indices that are definitely not in validators info file + if (!validatorsInfo.MayContainIndexInRange(startValidatorIndex, endValidatorIndex)) + { + err = ""; + return false; + } + + if (msg.Count == 0) + { + err = "Registration message has zero registration keys."; + return false; + } + + if (msg.Version != messageVersion) + { + err = $"Registration message has wrong version ({msg.Version}) should be {messageVersion}."; + return false; + } + + if (msg.ChainId != chainId) + { + err = $"Registration message has incorrect chain ID ({msg.ChainId}) should be {chainId}."; + return false; + } + + if (!msg.ContractAddress.SequenceEqual(ContractAddress!.Bytes)) + { + err = $"Registration message contains an invalid contract address ({msg.ContractAddress.ToHexString()}) should be {ContractAddress}."; + return false; + } + + BlsSigner.AggregatedPublicKey pk = new(stackalloc long[Bls.P1.Sz]); + for (ulong v = startValidatorIndex; v < endValidatorIndex; v++) + { + if (!validatorsInfo.ContainsIndex(v)) { - unregistered.Add(msg.ValidatorIndex); + err = $"Registration message contains a validator index that was not found in validator info file ({v})."; + return false; } + pk.Aggregate(validatorsInfo.GetPubKey(v)); } - return unregistered.Count == 0; + if (!ShutterCrypto.CheckValidatorRegistrySignatures(pk, update.Signature, update.Message)) + { + err = "Registration message has invalid signature."; + return false; + } + + err = ""; + return true; } - private readonly ref struct Message + internal readonly ref struct Message { public const int Sz = 46; - public readonly byte Version; - public readonly ulong ChainId; - public readonly ReadOnlySpan ContractAddress; - public readonly ulong ValidatorIndex; - public readonly ulong Nonce; - public readonly bool IsRegistration; + public readonly byte Version { get; init; } + public readonly ulong ChainId { get; init; } + public readonly ReadOnlySpan ContractAddress { get; init; } + public readonly ulong StartValidatorIndex { get; init; } + public readonly uint Count { get; init; } + public readonly uint Nonce { get; init; } + public readonly bool IsRegistration { get; init; } public Message(Span encodedMessage) { @@ -128,9 +181,23 @@ public Message(Span encodedMessage) Version = encodedMessage[0]; ChainId = BinaryPrimitives.ReadUInt64BigEndian(encodedMessage[1..]); ContractAddress = encodedMessage[9..29]; - ValidatorIndex = BinaryPrimitives.ReadUInt64BigEndian(encodedMessage[29..]); - Nonce = BinaryPrimitives.ReadUInt64BigEndian(encodedMessage[37..]); + StartValidatorIndex = BinaryPrimitives.ReadUInt64BigEndian(encodedMessage[29..37]); + Count = BinaryPrimitives.ReadUInt32BigEndian(encodedMessage[37..41]); + Nonce = BinaryPrimitives.ReadUInt32BigEndian(encodedMessage[41..45]); IsRegistration = encodedMessage[45] == 1; } + + internal byte[] Encode() + { + byte[] encoded = new byte[Sz]; + encoded[0] = Version; + BinaryPrimitives.WriteUInt64BigEndian(encoded.AsSpan()[1..], ChainId); + ContractAddress.CopyTo(encoded.AsSpan()[9..]); + BinaryPrimitives.WriteUInt64BigEndian(encoded.AsSpan()[29..], StartValidatorIndex); + BinaryPrimitives.WriteUInt32BigEndian(encoded.AsSpan()[37..], Count); + BinaryPrimitives.WriteUInt32BigEndian(encoded.AsSpan()[41..], Nonce); + encoded[45] = IsRegistration ? (byte)1 : (byte)0; + return encoded; + } } } diff --git a/src/Nethermind/Nethermind.Shutter/ShutterCrypto.cs b/src/Nethermind/Nethermind.Shutter/ShutterCrypto.cs index 5bacab9c531..4b6c5231fbf 100644 --- a/src/Nethermind/Nethermind.Shutter/ShutterCrypto.cs +++ b/src/Nethermind/Nethermind.Shutter/ShutterCrypto.cs @@ -216,9 +216,8 @@ public static bool CheckSlotDecryptionIdentitiesSignature( return expectedPubkey is not null && keyperAddress == expectedPubkey.Address; } - [SkipLocalsInit] - public static bool CheckValidatorRegistrySignature(G1Affine pk, ReadOnlySpan sigBytes, ReadOnlySpan msgBytes) - => BlsSigner.Verify(pk, sigBytes, ValueKeccak.Compute(msgBytes).Bytes); + public static bool CheckValidatorRegistrySignatures(BlsSigner.AggregatedPublicKey pk, ReadOnlySpan sigBytes, ReadOnlySpan msgBytes) + => BlsSigner.Verify(pk.PublicKey, sigBytes, ValueKeccak.Compute(msgBytes).Bytes); public static EncryptedMessage Encrypt(ReadOnlySpan msg, G1 identity, G2 eonKey, ReadOnlySpan sigma) { diff --git a/src/Nethermind/Nethermind.Shutter/ShutterPlugin.cs b/src/Nethermind/Nethermind.Shutter/ShutterPlugin.cs index 3f2e389463b..85ee2ac0a1d 100644 --- a/src/Nethermind/Nethermind.Shutter/ShutterPlugin.cs +++ b/src/Nethermind/Nethermind.Shutter/ShutterPlugin.cs @@ -89,7 +89,6 @@ public IBlockProducer InitBlockProducer(IBlockProducerFactory consensusPlugin, I try { validatorsInfo.Load(_shutterConfig!.ValidatorInfoFile); - validatorsInfo.Validate(); } catch (Exception e) {