RunReplayEngine.cs
1 using GUNRPG.Application.Backend; 2 using GUNRPG.Core.Operators; 3 using Microsoft.Extensions.Logging; 4 using Microsoft.Extensions.Logging.Abstractions; 5 6 namespace GUNRPG.Security; 7 8 public sealed class RunReplayEngine 9 { 10 private readonly ILogger<RunReplayEngine> _logger; 11 12 public RunReplayEngine(ILogger<RunReplayEngine>? logger = null) 13 { 14 _logger = logger ?? NullLogger<RunReplayEngine>.Instance; 15 } 16 17 public byte[] ValidateRunOnly(IReadOnlyList<OperatorEvent> events) 18 { 19 ArgumentNullException.ThrowIfNull(events); 20 21 var replayedAggregate = OperatorAggregate.FromEvents(events); 22 if (replayedAggregate.Events.Count != events.Count) 23 { 24 throw new InvalidOperationException( 25 $"Replay consumed only {replayedAggregate.Events.Count} of {events.Count} events; the event chain may be tampered."); 26 } 27 28 return OfflineMissionHashing.ComputeReplayFinalStateHash(replayedAggregate); 29 } 30 31 public RunValidationResult ValidateAndSignRun( 32 Guid runId, 33 Guid playerId, 34 IReadOnlyList<OperatorEvent> events, 35 ServerIdentity serverIdentity) 36 { 37 ArgumentNullException.ThrowIfNull(serverIdentity); 38 39 var finalStateHash = ValidateRunOnly(events); 40 var attestation = new SignedRunValidation( 41 serverIdentity.SignRunValidation(runId, playerId, finalStateHash), 42 serverIdentity.Certificate); 43 44 _logger.LogDebug("Run {RunId} validated and signed by server {ServerId}", runId, serverIdentity.Certificate.ServerId); 45 46 return new RunValidationResult( 47 runId, 48 playerId, 49 serverIdentity.Certificate.ServerId, 50 finalStateHash, 51 attestation); 52 } 53 }