From 84f5c3ec5efe5f5ac3a03839c78d9066070c6cf2 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill Date: Mon, 28 Oct 2024 16:39:35 +0000 Subject: [PATCH 1/8] cherry pick initial v1 impl --- .../ShutterCryptoTests.cs | 9 +-- .../Config/IShutterConfig.cs | 2 +- .../Config/ShutterConfig.cs | 2 +- .../Contracts/ValidatorRegistryContract.cs | 79 ++++++++++++++----- .../Nethermind.Shutter/ShutterCrypto.cs | 4 +- 5 files changed, 69 insertions(+), 27 deletions(-) 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/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/Contracts/ValidatorRegistryContract.cs b/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs index 9270a90ba32..557e5f7267a 100644 --- a/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs +++ b/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs @@ -14,6 +14,7 @@ using Update = (byte[] Message, byte[] Signature); using Nethermind.Crypto; using Nethermind.Shutter.Config; +using System.Linq; namespace Nethermind.Shutter.Contracts; @@ -55,10 +56,12 @@ public bool IsRegistered(in BlockHeader header, in ShutterValidatorsInfo validat } Message msg = new(update.Message.AsSpan()); + ulong startValidatorIndex = msg.StartValidatorIndex; + ulong endValidatorIndex = msg.StartValidatorIndex + msg.Count; - // skip untracked validators - if (!validatorsInfo.IsIndexRegistered(msg.ValidatorIndex)) + if (msg.Count == 0) { + if (_logger.IsDebug) _logger.Debug($"Registration message has zero registration keys"); continue; } @@ -80,28 +83,66 @@ public bool IsRegistered(in BlockHeader header, in ShutterValidatorsInfo validat continue; } - if (nonces[msg.ValidatorIndex].HasValue && msg.Nonce <= nonces[msg.ValidatorIndex]) + // only check validators in info file + bool untrackedValidator = false; + for (ulong v = msg.StartValidatorIndex; v < endValidatorIndex; v++) { - if (_logger.IsDebug) _logger.Debug($"Registration message has incorrect nonce ({msg.Nonce}) should be {nonces[msg.ValidatorIndex]}"); - continue; + if (!validatorsInfo.IsIndexRegistered(v)) + { + untrackedValidator = true; + break; + } } - - if (!ShutterCrypto.CheckValidatorRegistrySignature(validatorsInfo.GetPubKey(msg.ValidatorIndex), update.Signature, update.Message)) + if (untrackedValidator) { - if (_logger.IsDebug) _logger.Debug("Registration message has invalid signature."); continue; } - // message is valid - nonces[msg.ValidatorIndex] = msg.Nonce; + // if (!ShutterCrypto.CheckValidatorRegistrySignature(validatorsInfo.GetPubKey(msg.ValidatorIndex), update.Signature, update.Message)) + + // if (nonces[msg.ValidatorIndex].HasValue && msg.Nonce <= nonces[msg.ValidatorIndex]) + // { + // if (_logger.IsDebug) _logger.Debug($"Registration message has incorrect nonce ({msg.Nonce}) should be {nonces[msg.ValidatorIndex]}"); + // continue; + // } + + // todo: fix overflows + uint sz = BlsSigner.PkCompressedSz * msg.Count; + // using ArrayPoolList buf = new(sz, sz); + // Span publicKeys = new byte[sz]; - if (msg.IsRegistration) + // for (ulong v = startValidatorIndex; v < endValidatorIndex; v++) + // { + // validatorsInfo.GetPubKey(v).CopyTo(publicKeys[((int)v * BlsSigner.PkCompressedSz)..]); + // } + + // validatorsInfo.GetPubKey(msg.ValidatorIndex) + BlsSigner.AggregatedPublicKey aggregatedPublicKey = new(); + if (!ShutterCrypto.CheckValidatorRegistrySignatures(aggregatedPublicKey, update.Signature, update.Message)) { - unregistered.Remove(msg.ValidatorIndex); + if (_logger.IsDebug) _logger.Debug("Registration message has invalid signature."); + continue; } - else + + for (ulong v = startValidatorIndex; v < endValidatorIndex; v++) { - unregistered.Add(msg.ValidatorIndex); + if (nonces[v].HasValue && msg.Nonce <= nonces[v]) + { + if (_logger.IsDebug) _logger.Debug($"Registration message for validator index {v} has incorrect nonce ({msg.Nonce}) should be {nonces[v] + 1}"); + continue; + } + + // message is valid + nonces[v] = msg.Nonce; + + if (msg.IsRegistration) + { + unregistered.Remove(v); + } + else + { + unregistered.Add(v); + } } } @@ -114,8 +155,9 @@ private readonly ref struct Message public readonly byte Version; public readonly ulong ChainId; public readonly ReadOnlySpan ContractAddress; - public readonly ulong ValidatorIndex; - public readonly ulong Nonce; + public readonly ulong StartValidatorIndex; + public readonly uint Count; + public readonly uint Nonce; public readonly bool IsRegistration; public Message(Span encodedMessage) @@ -128,8 +170,9 @@ 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; } } diff --git a/src/Nethermind/Nethermind.Shutter/ShutterCrypto.cs b/src/Nethermind/Nethermind.Shutter/ShutterCrypto.cs index 5bacab9c531..488883634a3 100644 --- a/src/Nethermind/Nethermind.Shutter/ShutterCrypto.cs +++ b/src/Nethermind/Nethermind.Shutter/ShutterCrypto.cs @@ -217,8 +217,8 @@ public static bool CheckSlotDecryptionIdentitiesSignature( } [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) { From d2c5495f001518352c9a57521e08314baa98daf7 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill Date: Tue, 29 Oct 2024 16:14:31 +0000 Subject: [PATCH 2/8] fix aggregation --- src/Nethermind/Nethermind.Crypto/BlsSigner.cs | 5 +++ .../Config/ShutterValidatorsInfo.cs | 2 +- .../Contracts/ValidatorRegistryContract.cs | 32 +++++++------------ 3 files changed, 17 insertions(+), 22 deletions(-) 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/Config/ShutterValidatorsInfo.cs b/src/Nethermind/Nethermind.Shutter/Config/ShutterValidatorsInfo.cs index 98211c92e07..4386842eab7 100644 --- a/src/Nethermind/Nethermind.Shutter/Config/ShutterValidatorsInfo.cs +++ b/src/Nethermind/Nethermind.Shutter/Config/ShutterValidatorsInfo.cs @@ -41,7 +41,7 @@ public void Validate() } } - public bool IsIndexRegistered(ulong index) + public bool ContainsIndex(ulong index) => _indexToPubKeyBytes!.ContainsKey(index); public G1Affine GetPubKey(ulong index) diff --git a/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs b/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs index 557e5f7267a..1f0d58bb529 100644 --- a/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs +++ b/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs @@ -15,6 +15,7 @@ using Nethermind.Crypto; using Nethermind.Shutter.Config; using System.Linq; +using System.Runtime.CompilerServices; namespace Nethermind.Shutter.Contracts; @@ -34,6 +35,7 @@ public class ValidatorRegistryContract( public Update GetUpdate(BlockHeader header, in UInt256 i) => (Update)Call(header, nameof(GetUpdate), Address.Zero, [i])[0]; + [SkipLocalsInit] public bool IsRegistered(in BlockHeader header, in ShutterValidatorsInfo validatorsInfo, out HashSet unregistered) { Dictionary nonces = []; @@ -45,6 +47,8 @@ public bool IsRegistered(in BlockHeader header, in ShutterValidatorsInfo validat } uint updates = (uint)GetNumUpdates(header); + BlsSigner.AggregatedPublicKey pk = new(stackalloc long[Bls.P1.Sz]); + for (uint i = 0; i < updates; i++) { Update update = GetUpdate(header, updates - i - 1); @@ -87,7 +91,7 @@ public bool IsRegistered(in BlockHeader header, in ShutterValidatorsInfo validat bool untrackedValidator = false; for (ulong v = msg.StartValidatorIndex; v < endValidatorIndex; v++) { - if (!validatorsInfo.IsIndexRegistered(v)) + if (!validatorsInfo.ContainsIndex(v)) { untrackedValidator = true; break; @@ -98,27 +102,13 @@ public bool IsRegistered(in BlockHeader header, in ShutterValidatorsInfo validat continue; } - // if (!ShutterCrypto.CheckValidatorRegistrySignature(validatorsInfo.GetPubKey(msg.ValidatorIndex), update.Signature, update.Message)) - - // if (nonces[msg.ValidatorIndex].HasValue && msg.Nonce <= nonces[msg.ValidatorIndex]) - // { - // if (_logger.IsDebug) _logger.Debug($"Registration message has incorrect nonce ({msg.Nonce}) should be {nonces[msg.ValidatorIndex]}"); - // continue; - // } - - // todo: fix overflows - uint sz = BlsSigner.PkCompressedSz * msg.Count; - // using ArrayPoolList buf = new(sz, sz); - // Span publicKeys = new byte[sz]; - - // for (ulong v = startValidatorIndex; v < endValidatorIndex; v++) - // { - // validatorsInfo.GetPubKey(v).CopyTo(publicKeys[((int)v * BlsSigner.PkCompressedSz)..]); - // } + pk.Reset(); + for (ulong v = startValidatorIndex; v < endValidatorIndex; v++) + { + pk.Aggregate(validatorsInfo.GetPubKey(v)); + } - // validatorsInfo.GetPubKey(msg.ValidatorIndex) - BlsSigner.AggregatedPublicKey aggregatedPublicKey = new(); - if (!ShutterCrypto.CheckValidatorRegistrySignatures(aggregatedPublicKey, update.Signature, update.Message)) + if (!ShutterCrypto.CheckValidatorRegistrySignatures(pk, update.Signature, update.Message)) { if (_logger.IsDebug) _logger.Debug("Registration message has invalid signature."); continue; From 2fbc313fbadf842df5924a790cb42a0102fd22d8 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill Date: Tue, 29 Oct 2024 17:16:40 +0000 Subject: [PATCH 3/8] refactor validator registry contract --- .../Contracts/ValidatorRegistryContract.cs | 161 ++++++++++-------- 1 file changed, 94 insertions(+), 67 deletions(-) diff --git a/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs b/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs index 1f0d58bb529..182c72accb8 100644 --- a/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs +++ b/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs @@ -35,8 +35,10 @@ public class ValidatorRegistryContract( public Update GetUpdate(BlockHeader header, in UInt256 i) => (Update)Call(header, nameof(GetUpdate), Address.Zero, [i])[0]; - [SkipLocalsInit] 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 = []; @@ -46,97 +48,122 @@ public bool IsRegistered(in BlockHeader header, in ShutterValidatorsInfo validat unregistered.Add(index); } - uint updates = (uint)GetNumUpdates(header); - BlsSigner.AggregatedPublicKey pk = new(stackalloc long[Bls.P1.Sz]); - - 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()); - ulong startValidatorIndex = msg.StartValidatorIndex; - ulong endValidatorIndex = msg.StartValidatorIndex + msg.Count; + UpdateRegistrations(msg, nonces, unregistered); + } - if (msg.Count == 0) - { - if (_logger.IsDebug) _logger.Debug($"Registration message has zero registration keys"); - 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 - 1; + 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; - // only check validators in info file - bool untrackedValidator = false; - for (ulong v = msg.StartValidatorIndex; v < endValidatorIndex; v++) + if (msg.IsRegistration) { - if (!validatorsInfo.ContainsIndex(v)) - { - untrackedValidator = true; - break; - } + unregistered.Remove(v); } - if (untrackedValidator) + else { - continue; + unregistered.Add(v); } + } + } - pk.Reset(); - for (ulong v = startValidatorIndex; v < endValidatorIndex; v++) - { - pk.Aggregate(validatorsInfo.GetPubKey(v)); - } + [SkipLocalsInit] + 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 (!ShutterCrypto.CheckValidatorRegistrySignatures(pk, update.Signature, update.Message)) - { - if (_logger.IsDebug) _logger.Debug("Registration message has invalid signature."); - continue; - } + Message msg = new(update.Message.AsSpan()); + ulong startValidatorIndex = msg.StartValidatorIndex; + ulong endValidatorIndex = msg.StartValidatorIndex + msg.Count; + + 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; + } - for (ulong v = startValidatorIndex; v < endValidatorIndex; v++) + if (!msg.ContractAddress.SequenceEqual(ContractAddress!.Bytes)) + { + err = $"Registration message contains an invalid contract address ({msg.ContractAddress.ToHexString()}) should be {ContractAddress}."; + return false; + } + + // only check validators in info file + bool skip = true; + for (ulong v = msg.StartValidatorIndex; v < endValidatorIndex; v++) + { + if (validatorsInfo.ContainsIndex(v)) { - if (nonces[v].HasValue && msg.Nonce <= nonces[v]) - { - if (_logger.IsDebug) _logger.Debug($"Registration message for validator index {v} has incorrect nonce ({msg.Nonce}) should be {nonces[v] + 1}"); - continue; - } - - // message is valid - nonces[v] = msg.Nonce; - - if (msg.IsRegistration) - { - unregistered.Remove(v); - } - else - { - unregistered.Add(v); - } + skip = false; + break; } } - return unregistered.Count == 0; + if (skip) + { + err = ""; + return false; + } + + BlsSigner.AggregatedPublicKey pk = new(stackalloc long[Bls.P1.Sz]); + for (ulong v = startValidatorIndex; v < endValidatorIndex; v++) + { + pk.Aggregate(validatorsInfo.GetPubKey(v)); + } + + if (!ShutterCrypto.CheckValidatorRegistrySignatures(pk, update.Signature, update.Message)) + { + err = "Registration message has invalid signature."; + return false; + } + + err = ""; + return true; } private readonly ref struct Message From 0d1d6cff3c07d07f66e5977ab4b85e7f2ec2128b Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill Date: Tue, 29 Oct 2024 17:22:51 +0000 Subject: [PATCH 4/8] use non-inclusive end index consistently --- .../Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs b/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs index 182c72accb8..02fbe9f0446 100644 --- a/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs +++ b/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs @@ -74,8 +74,8 @@ internal bool IsRegistered(IEnumerable<(uint, Update)> updates, in ShutterValida private void UpdateRegistrations(Message msg, Dictionary nonces, HashSet unregistered) { - ulong endValidatorIndex = msg.StartValidatorIndex + msg.Count - 1; - for (ulong v = msg.StartValidatorIndex; v <= endValidatorIndex; v++) + ulong endValidatorIndex = msg.StartValidatorIndex + msg.Count; + for (ulong v = msg.StartValidatorIndex; v < endValidatorIndex; v++) { if (nonces[v].HasValue && msg.Nonce <= nonces[v]) { From 83750dc8ba9537f04acb6edb4966e42d03239ca6 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill Date: Wed, 30 Oct 2024 00:39:12 +0000 Subject: [PATCH 5/8] use range to check indices in validator info file --- .../Config/ShutterValidatorsInfo.cs | 10 ++++++- .../Contracts/ValidatorRegistryContract.cs | 27 +++++++------------ 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/Nethermind/Nethermind.Shutter/Config/ShutterValidatorsInfo.cs b/src/Nethermind/Nethermind.Shutter/Config/ShutterValidatorsInfo.cs index 4386842eab7..9fd642f51a8 100644 --- a/src/Nethermind/Nethermind.Shutter/Config/ShutterValidatorsInfo.cs +++ b/src/Nethermind/Nethermind.Shutter/Config/ShutterValidatorsInfo.cs @@ -6,7 +6,6 @@ using System.IO; using Nethermind.Crypto; using Nethermind.Serialization.Json; - using G1Affine = Nethermind.Crypto.Bls.P1Affine; namespace Nethermind.Shutter.Config; @@ -19,6 +18,8 @@ public class ShutterValidatorsInfoException(string message) : Exception(message) private Dictionary? _indexToPubKeyBytes; private readonly Dictionary _indexToPubKey = []; + private ulong _minIndex = ulong.MaxValue; + private ulong _maxIndex = ulong.MinValue; public void Load(string fp) { @@ -37,6 +38,9 @@ public void Validate() throw new ShutterValidatorsInfoException($"Validator info file contains invalid public key with index {index}."); } + _minIndex = Math.Min(_minIndex, index); + _maxIndex = Math.Max(_maxIndex, index + 1); + _indexToPubKey.Add(index, pk.Point.ToArray()); } } @@ -44,6 +48,10 @@ public void Validate() public bool ContainsIndex(ulong index) => _indexToPubKeyBytes!.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]); } diff --git a/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs b/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs index 02fbe9f0446..374e2892105 100644 --- a/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs +++ b/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs @@ -11,12 +11,13 @@ 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 System.Runtime.CompilerServices; +using Update = (byte[] Message, byte[] Signature); + namespace Nethermind.Shutter.Contracts; public class ValidatorRegistryContract( @@ -109,6 +110,13 @@ private bool IsUpdateValid(in Update update, in ShutterValidatorsInfo validators 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."; @@ -133,23 +141,6 @@ private bool IsUpdateValid(in Update update, in ShutterValidatorsInfo validators return false; } - // only check validators in info file - bool skip = true; - for (ulong v = msg.StartValidatorIndex; v < endValidatorIndex; v++) - { - if (validatorsInfo.ContainsIndex(v)) - { - skip = false; - break; - } - } - - if (skip) - { - err = ""; - return false; - } - BlsSigner.AggregatedPublicKey pk = new(stackalloc long[Bls.P1.Sz]); for (ulong v = startValidatorIndex; v < endValidatorIndex; v++) { From b1d0ba678e66362f5a897f9c80d3be3f19fa24f9 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill Date: Wed, 30 Oct 2024 11:24:51 +0000 Subject: [PATCH 6/8] add test for validator registration --- .../ShutterBlockHandlerTests.cs | 1 - .../ShutterValidatorRegistryTests.cs | 103 ++++++++++++++++++ .../Config/ShutterValidatorsInfo.cs | 50 +++++---- .../Contracts/ValidatorRegistryContract.cs | 31 ++++-- .../Nethermind.Shutter/ShutterPlugin.cs | 1 - 5 files changed, 151 insertions(+), 35 deletions(-) create mode 100644 src/Nethermind/Nethermind.Shutter.Test/ShutterValidatorRegistryTests.cs 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/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/ShutterValidatorsInfo.cs b/src/Nethermind/Nethermind.Shutter/Config/ShutterValidatorsInfo.cs index 9fd642f51a8..7455264fa72 100644 --- a/src/Nethermind/Nethermind.Shutter/Config/ShutterValidatorsInfo.cs +++ b/src/Nethermind/Nethermind.Shutter/Config/ShutterValidatorsInfo.cs @@ -12,46 +12,50 @@ 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 = []; - private ulong _minIndex = ulong.MaxValue; - private ulong _maxIndex = ulong.MinValue; + 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); + 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}."); } - _minIndex = Math.Min(_minIndex, index); - _maxIndex = Math.Max(_maxIndex, index + 1); - - _indexToPubKey.Add(index, pk.Point.ToArray()); + Add(index, pk.Point.ToArray()); } } - - public bool ContainsIndex(ulong index) - => _indexToPubKeyBytes!.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]); } diff --git a/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs b/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs index 374e2892105..a5b7616261f 100644 --- a/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs +++ b/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs @@ -14,7 +14,6 @@ using Nethermind.Crypto; using Nethermind.Shutter.Config; using System.Linq; -using System.Runtime.CompilerServices; using Update = (byte[] Message, byte[] Signature); @@ -97,7 +96,6 @@ private void UpdateRegistrations(Message msg, Dictionary nonces, } } - [SkipLocalsInit] private bool IsUpdateValid(in Update update, in ShutterValidatorsInfo validatorsInfo, out string err) { if (update.Message.Length != Message.Sz || update.Signature.Length != BlsSigner.Signature.Sz) @@ -157,16 +155,16 @@ private bool IsUpdateValid(in Update update, in ShutterValidatorsInfo validators 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 StartValidatorIndex; - public readonly uint Count; - public readonly uint 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) { @@ -183,5 +181,18 @@ public Message(Span encodedMessage) 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/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) { From 248fb24aa3e2a58ff3ed6e0976ea06f928cec477 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill Date: Fri, 1 Nov 2024 16:24:53 +0000 Subject: [PATCH 7/8] check if index in validator info file --- .../Contracts/ValidatorRegistryContract.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs b/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs index a5b7616261f..1c72f1ea222 100644 --- a/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs +++ b/src/Nethermind/Nethermind.Shutter/Contracts/ValidatorRegistryContract.cs @@ -142,6 +142,11 @@ private bool IsUpdateValid(in Update update, in ShutterValidatorsInfo validators BlsSigner.AggregatedPublicKey pk = new(stackalloc long[Bls.P1.Sz]); for (ulong v = startValidatorIndex; v < endValidatorIndex; v++) { + if (!validatorsInfo.ContainsIndex(v)) + { + err = $"Registration message contains a validator index that was not found in validator info file ({v})."; + return false; + } pk.Aggregate(validatorsInfo.GetPubKey(v)); } From 5be4902839caaf0e6a9ad7147224a2c566d2f67e Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill Date: Fri, 1 Nov 2024 16:26:53 +0000 Subject: [PATCH 8/8] anders comments --- .../Nethermind.Shutter/Config/ShutterValidatorsInfo.cs | 2 +- src/Nethermind/Nethermind.Shutter/ShutterCrypto.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Shutter/Config/ShutterValidatorsInfo.cs b/src/Nethermind/Nethermind.Shutter/Config/ShutterValidatorsInfo.cs index 7455264fa72..63d01db8421 100644 --- a/src/Nethermind/Nethermind.Shutter/Config/ShutterValidatorsInfo.cs +++ b/src/Nethermind/Nethermind.Shutter/Config/ShutterValidatorsInfo.cs @@ -22,7 +22,7 @@ public class ShutterValidatorsInfoException(string message) : Exception(message) public void Load(string fp) { - FileStream fstream = new(fp, FileMode.Open, FileAccess.Read, FileShare.None); + FileStream fstream = new(fp, FileMode.Open, FileAccess.Read, FileShare.Read); Dictionary indexToPubKeyBytes = new EthereumJsonSerializer().Deserialize>(fstream); AddPublicKeys(indexToPubKeyBytes); } diff --git a/src/Nethermind/Nethermind.Shutter/ShutterCrypto.cs b/src/Nethermind/Nethermind.Shutter/ShutterCrypto.cs index 488883634a3..4b6c5231fbf 100644 --- a/src/Nethermind/Nethermind.Shutter/ShutterCrypto.cs +++ b/src/Nethermind/Nethermind.Shutter/ShutterCrypto.cs @@ -216,7 +216,6 @@ public static bool CheckSlotDecryptionIdentitiesSignature( return expectedPubkey is not null && keyperAddress == expectedPubkey.Address; } - [SkipLocalsInit] public static bool CheckValidatorRegistrySignatures(BlsSigner.AggregatedPublicKey pk, ReadOnlySpan sigBytes, ReadOnlySpan msgBytes) => BlsSigner.Verify(pk.PublicKey, sigBytes, ValueKeccak.Compute(msgBytes).Bytes);