< Summary

Information
Class: NostrSure.Domain.Entities.NostrEventValidator
Assembly: NostrSure.Domain
File(s): /home/runner/work/NostrSure/NostrSure/NostrSure.Domain/Entities/NostrEventValidator.cs
Line coverage
60%
Covered lines: 93
Uncovered lines: 61
Coverable lines: 154
Total lines: 252
Line coverage: 60.3%
Branch coverage
80%
Covered branches: 40
Total branches: 50
Branch coverage: 80%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
ValidateAsync(...)100%11100%
Validate(...)50%11863.63%
ValidateSignature(...)95.45%242282.92%
ValidateEventId(...)100%2271.42%
ValidateKind(...)100%22100%
ValidateTags(...)91.66%121288.88%
CalculateEventId(...)100%210%
ParseHex(...)0%2040%
TryParseHex(...)100%210%

File(s)

/home/runner/work/NostrSure/NostrSure/NostrSure.Domain/Entities/NostrEventValidator.cs

#LineLine coverage
 1using NostrSure.Domain.Interfaces;
 2using NostrSure.Domain.Services;
 3using NostrSure.Domain.Validation;
 4using NostrSure.Domain.ValueObjects;
 5using System.Text;
 6using System.Text.Json;
 7
 8namespace NostrSure.Domain.Entities;
 9
 10/// <summary>
 11/// Legacy NostrEventValidator maintained for backward compatibility.
 12/// Consider migrating to ModularNostrEventValidator for better performance and testability.
 13/// </summary>
 14public sealed class NostrEventValidator : INostrEventValidator
 15{
 16    // Internal services for improved performance while maintaining compatibility
 17    private readonly IHexConverter _hexConverter;
 18    private readonly ICryptographicService _cryptographicService;
 19    private readonly IEventIdCalculator _eventIdCalculator;
 20
 2421    public NostrEventValidator()
 2422    {
 23        // Use optimized services internally for better performance
 2424        _hexConverter = new OptimizedHexConverter();
 2425        _cryptographicService = new OptimizedCryptographicService();
 2426        _eventIdCalculator = new SimpleEventIdCalculator();
 2427    }
 28
 29    #region New Async Methods
 30
 31    public Task<ValidationResult> ValidateAsync(NostrEvent evt, CancellationToken cancellationToken = default)
 132    {
 133        var result = Validate(evt);
 134        return Task.FromResult(result);
 135    }
 36
 37    public ValidationResult Validate(NostrEvent evt)
 338    {
 39        // Validate all components and return the first failure or success
 340        if (!ValidateKind(evt, out var kindError))
 041            return ValidationResult.Failure(kindError, "INVALID_KIND");
 42
 343        if (!ValidateTags(evt, out var tagError))
 144            return ValidationResult.Failure(tagError, "INVALID_TAGS");
 45
 246        if (!ValidateEventId(evt, out var eventIdError))
 247            return ValidationResult.Failure(eventIdError, "INVALID_EVENT_ID");
 48
 049        if (!ValidateSignature(evt, out var signatureError))
 050            return ValidationResult.Failure(signatureError, "INVALID_SIGNATURE");
 51
 052        return ValidationResult.Success();
 353    }
 54
 55    #endregion
 56
 57    #region Legacy Methods
 58
 59    public bool ValidateSignature(NostrEvent evt, out string error)
 960    {
 61        try
 962        {
 963            if (string.IsNullOrWhiteSpace(evt.Sig))
 164            {
 165                error = "Signature is empty.";
 166                return false;
 67            }
 868            if (string.IsNullOrWhiteSpace(evt.Pubkey?.Value))
 169            {
 170                error = "Pubkey is empty.";
 171                return false;
 72            }
 773            if (string.IsNullOrWhiteSpace(evt.Id))
 174            {
 175                error = "Event ID is empty.";
 176                return false;
 77            }
 78
 79            // Use optimized hex converter with stack allocation
 680            Span<byte> eventIdBytes = stackalloc byte[32];
 681            Span<byte> pubkeyBytes = stackalloc byte[32];
 682            Span<byte> sigBytes = stackalloc byte[64];
 83
 684            if (!_hexConverter.TryParseHex(evt.Id, eventIdBytes, out var eventIdLength) || eventIdLength != 32)
 185            {
 186                error = "Invalid event ID length (must be 32 bytes).";
 187                return false;
 88            }
 589            if (!_hexConverter.TryParseHex(evt.Pubkey.Value, pubkeyBytes, out var pubkeyLength) || pubkeyLength != 32)
 190            {
 191                error = "Invalid pubkey length for x-only pubkey (must be 32 bytes).";
 192                return false;
 93            }
 494            if (!_hexConverter.TryParseHex(evt.Sig, sigBytes, out var sigLength) || sigLength != 64)
 195            {
 196                error = "Invalid signature length for Schnorr signature (must be 64 bytes).";
 197                return false;
 98            }
 99
 100            // Use optimized cryptographic service
 3101            bool valid = _cryptographicService.VerifySchnorrSignature(sigBytes, eventIdBytes, pubkeyBytes);
 3102            if (!valid)
 0103            {
 0104                error = "Signature verification failed.";
 0105                return false;
 106            }
 107
 3108            error = string.Empty;
 3109            return true;
 110        }
 0111        catch (Exception ex)
 0112        {
 0113            error = $"Exception during signature validation: {ex.Message}";
 0114            return false;
 115        }
 9116    }
 117
 118    public bool ValidateEventId(NostrEvent evt, out string error)
 7119    {
 120        try
 7121        {
 7122            var calculatedId = _eventIdCalculator.CalculateEventId(evt);
 7123            if (evt.Id != calculatedId)
 3124            {
 3125                error = $"Event ID mismatch. Expected: {calculatedId}, Got: {evt.Id}";
 3126                return false;
 127            }
 4128            error = string.Empty;
 4129            return true;
 130        }
 0131        catch (Exception ex)
 0132        {
 0133            error = $"Exception during event ID validation: {ex.Message}";
 0134            return false;
 135        }
 7136    }
 137
 138    public bool ValidateKind(NostrEvent evt, out string error)
 5139    {
 5140        if (!Enum.IsDefined(typeof(EventKind), evt.Kind))
 1141        {
 1142            error = $"Unknown event kind: {evt.Kind}";
 1143            return false;
 144        }
 4145        error = string.Empty;
 4146        return true;
 5147    }
 148
 149    public bool ValidateTags(NostrEvent evt, out string error)
 11150    {
 11151        if (evt.Tags == null)
 1152        {
 1153            error = "Tags are null.";
 1154            return false;
 155        }
 156
 44157        foreach (var tag in evt.Tags)
 9158        {
 9159            if (tag == null)
 1160            {
 1161                error = "Tag is null.";
 1162                return false;
 163            }
 164
 8165            if (string.IsNullOrWhiteSpace(tag.Name))
 0166            {
 0167                error = "Tag name is empty or null.";
 0168                return false;
 169            }
 170
 8171            if (!tag.IsValid())
 2172            {
 2173                error = $"Invalid tag: {tag.Name}";
 2174                return false;
 175            }
 176
 6177            if (tag.Values.Any(string.IsNullOrWhiteSpace))
 1178            {
 1179                error = "Tag contains empty value.";
 1180                return false;
 181            }
 5182        }
 183
 6184        error = string.Empty;
 6185        return true;
 11186    }
 187
 188    #endregion
 189
 190    #region Legacy Helper Methods (Deprecated)
 191
 192    [Obsolete("This method uses legacy hex parsing. Consider using ModularNostrEventValidator for better performance.")]
 193    private string CalculateEventId(NostrEvent evt)
 0194    {
 195        // Keep legacy implementation for backward compatibility
 0196        var options = new JsonSerializerOptions
 0197        {
 0198            WriteIndented = false,
 0199            Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
 0200        };
 201
 0202        var tagsArrays = evt.Tags.Select(tag =>
 0203        {
 0204            var array = new List<string> { tag.Name };
 0205            array.AddRange(tag.Values);
 0206            return array.ToArray();
 0207        }).ToArray();
 208
 0209        var eventArray = new object[]
 0210        {
 0211            0,
 0212            evt.Pubkey.Value,
 0213            evt.CreatedAt.ToUnixTimeSeconds(),
 0214            (int)evt.Kind,
 0215            tagsArrays,
 0216            evt.Content
 0217        };
 218
 0219        var serialized = JsonSerializer.Serialize(eventArray, options);
 0220        var utf8Bytes = Encoding.UTF8.GetBytes(serialized);
 0221        var hash = NBitcoin.Crypto.Hashes.SHA256(utf8Bytes);
 0222        return Convert.ToHexString(hash).ToLowerInvariant();
 0223    }
 224
 225    [Obsolete("This method uses legacy hex parsing. Consider using OptimizedHexConverter for better performance.")]
 226    private static byte[] ParseHex(string hex)
 0227    {
 0228        if (hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
 0229            hex = hex.Substring(2);
 0230        var bytes = new byte[hex.Length / 2];
 0231        for (int i = 0; i < bytes.Length; i++)
 0232            bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
 0233        return bytes;
 0234    }
 235
 236    [Obsolete("This method is unused. Consider using OptimizedHexConverter.TryParseHex instead.")]
 237    private static bool TryParseHex(string hex, out byte[] bytes)
 0238    {
 239        try
 0240        {
 0241            bytes = ParseHex(hex);
 0242            return true;
 243        }
 0244        catch
 0245        {
 0246            bytes = Array.Empty<byte>();
 0247            return false;
 248        }
 0249    }
 250
 251    #endregion
 252}