sprints work

This commit is contained in:
StellaOps Bot
2025-12-25 12:19:12 +02:00
parent 223843f1d1
commit 2a06f780cf
224 changed files with 41796 additions and 1515 deletions

View File

@@ -0,0 +1,336 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using StellaOps.Provcache.Entities;
namespace StellaOps.Provcache.Postgres;
/// <summary>
/// PostgreSQL implementation of <see cref="IProvcacheRepository"/>.
/// </summary>
public sealed class PostgresProvcacheRepository : IProvcacheRepository
{
private readonly ProvcacheDbContext _context;
private readonly ILogger<PostgresProvcacheRepository> _logger;
private readonly JsonSerializerOptions _jsonOptions;
public PostgresProvcacheRepository(
ProvcacheDbContext context,
ILogger<PostgresProvcacheRepository> logger)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
}
/// <inheritdoc />
public async Task<ProvcacheEntry?> GetAsync(string veriKey, CancellationToken cancellationToken = default)
{
var entity = await _context.ProvcacheItems
.AsNoTracking()
.FirstOrDefaultAsync(e => e.VeriKey == veriKey, cancellationToken)
.ConfigureAwait(false);
return entity is null ? null : MapToEntry(entity);
}
/// <inheritdoc />
public async Task<IReadOnlyDictionary<string, ProvcacheEntry>> GetManyAsync(
IEnumerable<string> veriKeys,
CancellationToken cancellationToken = default)
{
var keyList = veriKeys.ToList();
if (keyList.Count == 0)
return new Dictionary<string, ProvcacheEntry>();
var entities = await _context.ProvcacheItems
.AsNoTracking()
.Where(e => keyList.Contains(e.VeriKey))
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return entities.ToDictionary(e => e.VeriKey, MapToEntry);
}
/// <inheritdoc />
public async Task UpsertAsync(ProvcacheEntry entry, CancellationToken cancellationToken = default)
{
var entity = MapToEntity(entry);
var existing = await _context.ProvcacheItems
.FirstOrDefaultAsync(e => e.VeriKey == entry.VeriKey, cancellationToken)
.ConfigureAwait(false);
if (existing is null)
{
_context.ProvcacheItems.Add(entity);
}
else
{
_context.Entry(existing).CurrentValues.SetValues(entity);
}
await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task UpsertManyAsync(IEnumerable<ProvcacheEntry> entries, CancellationToken cancellationToken = default)
{
var entryList = entries.ToList();
if (entryList.Count == 0)
return;
var veriKeys = entryList.Select(e => e.VeriKey).ToList();
var existing = await _context.ProvcacheItems
.Where(e => veriKeys.Contains(e.VeriKey))
.ToDictionaryAsync(e => e.VeriKey, cancellationToken)
.ConfigureAwait(false);
foreach (var entry in entryList)
{
var entity = MapToEntity(entry);
if (existing.TryGetValue(entry.VeriKey, out var existingEntity))
{
_context.Entry(existingEntity).CurrentValues.SetValues(entity);
}
else
{
_context.ProvcacheItems.Add(entity);
}
}
await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<bool> DeleteAsync(string veriKey, CancellationToken cancellationToken = default)
{
var entity = await _context.ProvcacheItems
.FirstOrDefaultAsync(e => e.VeriKey == veriKey, cancellationToken)
.ConfigureAwait(false);
if (entity is null)
return false;
_context.ProvcacheItems.Remove(entity);
await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
return true;
}
/// <inheritdoc />
public async Task<long> DeleteByPolicyHashAsync(string policyHash, CancellationToken cancellationToken = default)
{
var deleted = await _context.ProvcacheItems
.Where(e => e.PolicyHash == policyHash)
.ExecuteDeleteAsync(cancellationToken)
.ConfigureAwait(false);
if (deleted > 0)
{
await LogRevocationAsync("policy", policyHash, "policy-update", deleted, cancellationToken)
.ConfigureAwait(false);
}
return deleted;
}
/// <inheritdoc />
public async Task<long> DeleteBySignerSetHashAsync(string signerSetHash, CancellationToken cancellationToken = default)
{
var deleted = await _context.ProvcacheItems
.Where(e => e.SignerSetHash == signerSetHash)
.ExecuteDeleteAsync(cancellationToken)
.ConfigureAwait(false);
if (deleted > 0)
{
await LogRevocationAsync("signer", signerSetHash, "signer-revocation", deleted, cancellationToken)
.ConfigureAwait(false);
}
return deleted;
}
/// <inheritdoc />
public async Task<long> DeleteByFeedEpochOlderThanAsync(string feedEpoch, CancellationToken cancellationToken = default)
{
var deleted = await _context.ProvcacheItems
.Where(e => string.Compare(e.FeedEpoch, feedEpoch, StringComparison.Ordinal) < 0)
.ExecuteDeleteAsync(cancellationToken)
.ConfigureAwait(false);
if (deleted > 0)
{
await LogRevocationAsync("feed", feedEpoch, "feed-update", deleted, cancellationToken)
.ConfigureAwait(false);
}
return deleted;
}
/// <inheritdoc />
public async Task<long> DeleteExpiredAsync(DateTimeOffset asOf, CancellationToken cancellationToken = default)
{
var deleted = await _context.ProvcacheItems
.Where(e => e.ExpiresAt <= asOf)
.ExecuteDeleteAsync(cancellationToken)
.ConfigureAwait(false);
if (deleted > 0)
{
await LogRevocationAsync("expired", asOf.ToString("O"), "ttl-expiry", deleted, cancellationToken)
.ConfigureAwait(false);
}
return deleted;
}
/// <inheritdoc />
public async Task IncrementHitCountAsync(string veriKey, CancellationToken cancellationToken = default)
{
await _context.ProvcacheItems
.Where(e => e.VeriKey == veriKey)
.ExecuteUpdateAsync(
setters => setters
.SetProperty(e => e.HitCount, e => e.HitCount + 1)
.SetProperty(e => e.LastAccessedAt, DateTimeOffset.UtcNow),
cancellationToken)
.ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<ProvcacheStatistics> GetStatisticsAsync(CancellationToken cancellationToken = default)
{
var now = DateTimeOffset.UtcNow;
var hourFromNow = now.AddHours(1);
var totalEntries = await _context.ProvcacheItems
.LongCountAsync(cancellationToken)
.ConfigureAwait(false);
var totalHits = await _context.ProvcacheItems
.SumAsync(e => e.HitCount, cancellationToken)
.ConfigureAwait(false);
var expiringWithinHour = await _context.ProvcacheItems
.LongCountAsync(e => e.ExpiresAt <= hourFromNow, cancellationToken)
.ConfigureAwait(false);
var uniquePolicies = await _context.ProvcacheItems
.Select(e => e.PolicyHash)
.Distinct()
.CountAsync(cancellationToken)
.ConfigureAwait(false);
var uniqueSignerSets = await _context.ProvcacheItems
.Select(e => e.SignerSetHash)
.Distinct()
.CountAsync(cancellationToken)
.ConfigureAwait(false);
var oldest = await _context.ProvcacheItems
.OrderBy(e => e.CreatedAt)
.Select(e => (DateTimeOffset?)e.CreatedAt)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
var newest = await _context.ProvcacheItems
.OrderByDescending(e => e.CreatedAt)
.Select(e => (DateTimeOffset?)e.CreatedAt)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
return new ProvcacheStatistics
{
TotalEntries = totalEntries,
TotalHits = totalHits,
ExpiringWithinHour = expiringWithinHour,
UniquePolicies = uniquePolicies,
UniqueSignerSets = uniqueSignerSets,
OldestEntry = oldest,
NewestEntry = newest
};
}
private async Task LogRevocationAsync(
string type,
string targetHash,
string reason,
long entriesAffected,
CancellationToken cancellationToken)
{
var revocation = new ProvcacheRevocationEntity
{
RevocationId = Guid.NewGuid(),
RevocationType = type,
TargetHash = targetHash,
Reason = reason,
EntriesAffected = entriesAffected,
CreatedAt = DateTimeOffset.UtcNow
};
_context.Revocations.Add(revocation);
await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
_logger.LogInformation(
"Logged revocation: type={Type}, target={TargetHash}, affected={EntriesAffected}",
type,
targetHash,
entriesAffected);
}
private ProvcacheEntry MapToEntry(ProvcacheItemEntity entity)
{
var replaySeed = JsonSerializer.Deserialize<ReplaySeed>(entity.ReplaySeed, _jsonOptions)
?? new ReplaySeed { FeedIds = [], RuleIds = [] };
return new ProvcacheEntry
{
VeriKey = entity.VeriKey,
Decision = new DecisionDigest
{
DigestVersion = entity.DigestVersion,
VeriKey = entity.VeriKey,
VerdictHash = entity.VerdictHash,
ProofRoot = entity.ProofRoot,
ReplaySeed = replaySeed,
CreatedAt = entity.CreatedAt,
ExpiresAt = entity.ExpiresAt,
TrustScore = entity.TrustScore
},
PolicyHash = entity.PolicyHash,
SignerSetHash = entity.SignerSetHash,
FeedEpoch = entity.FeedEpoch,
CreatedAt = entity.CreatedAt,
ExpiresAt = entity.ExpiresAt,
HitCount = entity.HitCount,
LastAccessedAt = entity.LastAccessedAt
};
}
private ProvcacheItemEntity MapToEntity(ProvcacheEntry entry)
{
return new ProvcacheItemEntity
{
VeriKey = entry.VeriKey,
DigestVersion = entry.Decision.DigestVersion,
VerdictHash = entry.Decision.VerdictHash,
ProofRoot = entry.Decision.ProofRoot,
ReplaySeed = JsonSerializer.Serialize(entry.Decision.ReplaySeed, _jsonOptions),
PolicyHash = entry.PolicyHash,
SignerSetHash = entry.SignerSetHash,
FeedEpoch = entry.FeedEpoch,
TrustScore = entry.Decision.TrustScore,
HitCount = entry.HitCount,
CreatedAt = entry.CreatedAt,
ExpiresAt = entry.ExpiresAt,
UpdatedAt = DateTimeOffset.UtcNow,
LastAccessedAt = entry.LastAccessedAt
};
}
}

View File

@@ -0,0 +1,58 @@
using Microsoft.EntityFrameworkCore;
using StellaOps.Provcache.Entities;
namespace StellaOps.Provcache.Postgres;
/// <summary>
/// EF Core DbContext for Provcache storage.
/// </summary>
public class ProvcacheDbContext : DbContext
{
public ProvcacheDbContext(DbContextOptions<ProvcacheDbContext> options) : base(options)
{
}
public DbSet<ProvcacheItemEntity> ProvcacheItems => Set<ProvcacheItemEntity>();
public DbSet<ProvcacheEvidenceChunkEntity> EvidenceChunks => Set<ProvcacheEvidenceChunkEntity>();
public DbSet<ProvcacheRevocationEntity> Revocations => Set<ProvcacheRevocationEntity>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.HasDefaultSchema("provcache");
// ProvcacheItemEntity configuration
modelBuilder.Entity<ProvcacheItemEntity>(entity =>
{
entity.HasKey(e => e.VeriKey);
entity.HasIndex(e => e.PolicyHash);
entity.HasIndex(e => e.SignerSetHash);
entity.HasIndex(e => e.FeedEpoch);
entity.HasIndex(e => e.ExpiresAt);
entity.HasIndex(e => e.CreatedAt);
entity.Property(e => e.ReplaySeed)
.HasColumnType("jsonb");
});
// ProvcacheEvidenceChunkEntity configuration
modelBuilder.Entity<ProvcacheEvidenceChunkEntity>(entity =>
{
entity.HasKey(e => e.ChunkId);
entity.HasIndex(e => e.ProofRoot);
entity.HasIndex(e => new { e.ProofRoot, e.ChunkIndex }).IsUnique();
});
// ProvcacheRevocationEntity configuration
modelBuilder.Entity<ProvcacheRevocationEntity>(entity =>
{
entity.HasKey(e => e.RevocationId);
entity.HasIndex(e => e.CreatedAt);
entity.HasIndex(e => e.TargetHash);
});
}
}

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<RootNamespace>StellaOps.Provcache.Postgres</RootNamespace>
<AssemblyName>StellaOps.Provcache.Postgres</AssemblyName>
<Description>PostgreSQL storage implementation for StellaOps Provcache</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../StellaOps.Provcache/StellaOps.Provcache.csproj" />
</ItemGroup>
</Project>