Skip to content

Commit

Permalink
Multiple fixes:
Browse files Browse the repository at this point in the history
- Fixed ordering of the scoreboard. People with faster solves will be higher.
- Added point adjustments for new baseline submissions.
- Implemented fixed ordering for CTFtime endpoints.
- Fixed colours on team detail page.
  • Loading branch information
Emzi0767 committed Oct 9, 2020
1 parent be8904b commit 7fc56e4
Show file tree
Hide file tree
Showing 19 changed files with 386 additions and 21 deletions.
23 changes: 11 additions & 12 deletions src/RosettaCTF.API/Controllers/ChallengesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public sealed class ChallengesController : RosettaControllerBase
private ICtfChallengeRepository ChallengeRepository { get; }
private ChallengePreviewRepository ChallengePreviewRepository { get; }
private ICtfChallengeCacheRepository ChallengeCacheRepository { get; }
private IScoringModel ScoringModel { get; }
private ScoreCalculatorService ScoreCalculator { get; }

public ChallengesController(
ILoggerFactory loggerFactory,
Expand All @@ -50,13 +50,13 @@ public ChallengesController(
ICtfChallengeRepository challengeRepository,
ChallengePreviewRepository challengePreviewRepository,
ICtfChallengeCacheRepository challengeCacheRepository,
IScoringModel scoringModel)
ScoreCalculatorService scoreCalculator)
: base(loggerFactory, userRepository, userPreviewRepository, ctfConfigurationLoader)
{
this.ChallengeRepository = challengeRepository;
this.ChallengePreviewRepository = challengePreviewRepository;
this.ChallengeCacheRepository = challengeCacheRepository;
this.ScoringModel = scoringModel;
this.ScoreCalculator = scoreCalculator;
}

[HttpGet]
Expand Down Expand Up @@ -137,25 +137,24 @@ public async Task<ActionResult<ApiResult<bool>>> SubmitFlag([FromRoute] string i

if (valid && this.EventConfiguration.Scoring != CtfScoringMode.Static)
{
var solves = await this.ChallengeCacheRepository.IncrementSolveCountAsync(challenge.Id, cancellationToken);
var baseline = await this.ChallengeCacheRepository.GetBaselineSolveCountAsync(cancellationToken);
var rate = solves / (double)baseline;
var postRate = (solves + 1.0) / baseline;
var cscore = this.ScoringModel.ComputeScore(challenge.BaseScore, rate);
var pscore = this.ScoringModel.ComputeScore(challenge.BaseScore, postRate);

await this.ChallengeCacheRepository.UpdateScoreAsync(challenge.Id, pscore, cancellationToken);
var scoreInfo = await this.ScoreCalculator.ComputeCurrentScoreAsync(challenge, cancellationToken);

if (this.EventConfiguration.Scoring == CtfScoringMode.Freezer)
score = cscore;
score = scoreInfo.Current;
}

var solve = await this.ChallengeRepository.SubmitSolveAsync(flag, valid, challenge.Id, this.RosettaUser.Id, this.RosettaUser.Team.Id, score, cancellationToken);
if (solve == null)
return this.Conflict(ApiResult.FromError<bool>(new ApiError(ApiErrorCode.AlreadySolved, "Your team already solved this challenge.")));

if (valid && challenge.BaseScore == 1)
{
await this.ChallengeCacheRepository.IncrementBaselineSolveCountAsync(cancellationToken);

if (this.EventConfiguration.Scoring != CtfScoringMode.Static)
await this.ScoreCalculator.UpdateAllScoresAsync(this.EventConfiguration.Scoring == CtfScoringMode.Freezer, false, cancellationToken);
}

return this.Ok(ApiResult.FromResult(valid));
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/RosettaCTF.API/Controllers/CtfTimeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public sealed class CtfTimeController : RosettaControllerBase
{
private ICtfChallengeRepository ChallengeRepository { get; }
private ICtfChallengeCacheRepository ChallengeCacheRepository { get; }
private ScoreCalculatorService ScoreCalculator { get; }

public CtfTimeController(
ILoggerFactory loggerFactory,
Expand Down Expand Up @@ -62,6 +63,7 @@ public async Task<ActionResult<CtfTimeScoreboard>> Scoreboard(CancellationToken
Tasks = challenges.Select(x => x.Title),
Standings = this.CreateStandings(solves.GroupBy(x => x.Team), points)
.OrderByDescending(x => x.Score)
.ThenBy(x => x.LastAccept)
.Select((x, i) => { x.Pos = i + 1; return x; })
};
return this.Ok(scoreboard);
Expand Down
30 changes: 30 additions & 0 deletions src/RosettaCTF.API/Models/ScoreInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// This file is part of RosettaCTF project.
//
// Copyright 2020 Emzi0767
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

namespace RosettaCTF.Models
{
public struct ScoreInfo
{
public int Current { get; }
public int Next { get; }

public ScoreInfo(int current, int next)
{
this.Current = current;
this.Next = next;
}
}
}
36 changes: 36 additions & 0 deletions src/RosettaCTF.API/Models/ScoreLock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// This file is part of RosettaCTF project.
//
// Copyright 2020 Emzi0767
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Threading;

namespace RosettaCTF.Models
{
public sealed class ScoreLock : IDisposable
{
private SemaphoreSlim Semaphore { get; }

public ScoreLock(SemaphoreSlim semaphore)
{
this.Semaphore = semaphore;
}

public void Dispose()
{
this.Semaphore.Release(1);
}
}
}
5 changes: 5 additions & 0 deletions src/RosettaCTF.API/Services/ChallengeBootstrapperService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,16 @@ public async Task StartAsync(CancellationToken cancellationToken)
var repository = services.GetRequiredService<ICtfChallengeRepository>();
var configLoader = services.GetRequiredService<ICtfConfigurationLoader>();
var cache = services.GetRequiredService<ICtfChallengeCacheRepository>();
var scorer = services.GetRequiredService<ScoreCalculatorService>();
var challenges = configLoader.LoadChallenges();
var @event = configLoader.LoadEventData();

await repository.InstallAsync(challenges, cancellationToken);
await cache.InstallAsync(challenges.SelectMany(x => x.Challenges)
.ToDictionary(x => x.Id, x => x.BaseScore), cancellationToken);

if (@event.Scoring != CtfScoringMode.Static)
await scorer.UpdateAllScoresAsync(@event.Scoring == CtfScoringMode.Freezer, true, cancellationToken);
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/RosettaCTF.API/Services/ChallengePreviewRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,10 @@ public IEnumerable<ScoreboardEntryPreview> GetScoreboard(
if (score == 0 || score == null)
score = x.Sum(x => scores?[x.Challenge.Id] ?? 0);

return new { team = teams[x.Key], score = score.Value };
return new { team = teams[x.Key], score = score.Value, lastAccept = x.Last().Timestamp };
})
.OrderByDescending(x => x.score)
.ThenBy(x => x.lastAccept)
.Select((x, i) => new ScoreboardEntryPreview(x.team, x.score, i + 1, null))
.ToList();

Expand Down
143 changes: 143 additions & 0 deletions src/RosettaCTF.API/Services/ScoreCalculatorService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// This file is part of RosettaCTF project.
//
// Copyright 2020 Emzi0767
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using RosettaCTF.Data;
using RosettaCTF.Models;

namespace RosettaCTF.Services
{
/// <summary>
/// Handles score computations.
/// </summary>
public sealed class ScoreCalculatorService
{
private ICtfChallengeRepository ChallengeRepository { get; }
private ICtfChallengeCacheRepository ChallengeCacheRepository { get; }
private IScoringModel ScoringModel { get; }
private ScoreLockService ScoreLockService { get; }

public ScoreCalculatorService(
ICtfChallengeRepository ctfChallengeRepository,
ICtfChallengeCacheRepository ctfChallengeCacheRepository,
IScoringModel scoringModel,
ScoreLockService scoreLockService)
{
this.ChallengeRepository = ctfChallengeRepository;
this.ChallengeCacheRepository = ctfChallengeCacheRepository;
this.ScoringModel = scoringModel;
this.ScoreLockService = scoreLockService;
}

public async Task<ScoreInfo> ComputeCurrentScoreAsync(ICtfChallenge challenge, CancellationToken cancellationToken = default)
{
using var _ = await this.ScoreLockService.AcquireLockAsync(challenge.Id, cancellationToken);

var solves = await this.ChallengeCacheRepository.IncrementSolveCountAsync(challenge.Id, cancellationToken);
var baseline = await this.ChallengeCacheRepository.GetBaselineSolveCountAsync(cancellationToken);
var rate = solves / (double)baseline;
var postRate = (solves + 1.0) / baseline;
var cscore = this.ScoringModel.ComputeScore(challenge.BaseScore, rate);
var pscore = this.ScoringModel.ComputeScore(challenge.BaseScore, postRate);

await this.ChallengeCacheRepository.UpdateScoreAsync(challenge.Id, pscore, cancellationToken);
return new ScoreInfo(cscore, pscore);
}

public async Task UpdateAllScoresAsync(bool freezeScores, bool includeBaseline, CancellationToken cancellationToken = default)
{
var baseline = includeBaseline
? await this.RecountBaselineAsync(cancellationToken)
: await this.ChallengeCacheRepository.GetBaselineSolveCountAsync(cancellationToken);

if (includeBaseline)
await this.ChallengeCacheRepository.SetBaselineSolveCountAsync(baseline, cancellationToken);

await this.UpdateAllScoresJeopardyAsync(baseline, cancellationToken);
if (freezeScores)
await this.UpdateAllScoresFreezerAsync(baseline, cancellationToken);
}

private async Task<int> RecountBaselineAsync(CancellationToken cancellationToken = default)
{
var solves = await this.ChallengeRepository.GetSuccessfulSolvesAsync(cancellationToken);
return solves.Count(x => x.Challenge.BaseScore <= 1);
}

private async Task UpdateAllScoresJeopardyAsync(double baseline, CancellationToken cancellationToken = default)
{
var challenges = await this.ChallengeRepository.GetChallengesAsync(cancellationToken);
challenges = challenges.Where(x => x.BaseScore > 1);
var locks = await Task.WhenAll(challenges.Select(x => this.ScoreLockService.AcquireLockAsync(x.Id, cancellationToken)));

try
{
foreach (var challenge in challenges)
{
var solves = await this.ChallengeCacheRepository.GetSolveCountAsync(challenge.Id, cancellationToken);
var rate = solves / baseline;
var score = this.ScoringModel.ComputeScore(challenge.BaseScore, rate);
await this.ChallengeCacheRepository.UpdateScoreAsync(challenge.Id, score, cancellationToken);
}
}
finally
{
foreach (var @lock in locks)
@lock.Dispose();
}
}

private async Task UpdateAllScoresFreezerAsync(double baseline, CancellationToken cancellationToken = default)
{
var solves = await this.ChallengeRepository.GetSuccessfulSolvesAsync(cancellationToken);
solves.Where(x => x.Challenge.BaseScore > 1);
var locks = await Task.WhenAll(solves.Select(x => x.Challenge.Id)
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(x => this.ScoreLockService.AcquireLockAsync(x, cancellationToken)));

try
{
var updates = this.CreateUpdates(solves, baseline);
await this.ChallengeRepository.UpdateSolvesAsync(updates, cancellationToken);


}
finally
{
foreach (var @lock in locks)
@lock.Dispose();
}
}

private IEnumerable<CtfSolveUpdate> CreateUpdates(IEnumerable<ICtfSolveSubmission> solves, double baseline)
{
foreach (var solveGroup in solves.GroupBy(x => x.Challenge.Id, StringComparer.OrdinalIgnoreCase))
{
var i = 0;
foreach (var solve in solveGroup)
{
var rate = i++ / baseline;
var score = this.ScoringModel.ComputeScore(solve.Challenge.BaseScore, rate);
yield return new CtfSolveUpdate(solve, score);
}
}
}
}
}
43 changes: 43 additions & 0 deletions src/RosettaCTF.API/Services/ScoreLockService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// This file is part of RosettaCTF project.
//
// Copyright 2020 Emzi0767
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using RosettaCTF.Models;

namespace RosettaCTF.Services
{
public sealed class ScoreLockService
{
private ConcurrentDictionary<string, SemaphoreSlim> ScoreSemaphores { get; }

public ScoreLockService()
{
this.ScoreSemaphores = new ConcurrentDictionary<string, SemaphoreSlim>(StringComparer.OrdinalIgnoreCase);
}

public async Task<ScoreLock> AcquireLockAsync(string challengeId, CancellationToken cancellationToken = default)
{
if (!this.ScoreSemaphores.TryGetValue(challengeId, out var semaphore))
semaphore = this.ScoreSemaphores.GetOrAdd(challengeId, new SemaphoreSlim(1, 1));

await semaphore.WaitAsync(cancellationToken);
return new ScoreLock(semaphore);
}
}
}
4 changes: 3 additions & 1 deletion src/RosettaCTF.API/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,9 @@ public void ConfigureServices(IServiceCollection services)
.AddSingleton<OAuthConfigurationProvider>()
.AddScoped<OAuthProviderSelector>()
.AddScoped<LoginSettingsRepository>()
.AddTransient<MfaValidatorService>();
.AddTransient<MfaValidatorService>()
.AddScoped<ScoreCalculatorService>()
.AddSingleton<ScoreLockService>();

services.AddHostedService<ChallengeBootstrapperService>();

Expand Down
Loading

0 comments on commit 7fc56e4

Please sign in to comment.