sprints enhancements
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -14,7 +15,7 @@ public sealed class ProvcacheApiEndpoints;
|
||||
/// <summary>
|
||||
/// Extension methods for mapping Provcache API endpoints.
|
||||
/// </summary>
|
||||
public static class ProvcacheEndpointExtensions
|
||||
public static partial class ProvcacheEndpointExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps Provcache API endpoints to the specified route builder.
|
||||
@@ -69,6 +70,47 @@ public static class ProvcacheEndpointExtensions
|
||||
.Produces<ProvcacheMetricsResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
|
||||
// Map evidence paging endpoints under /proofs
|
||||
var proofsGroup = endpoints.MapGroup($"{prefix}/proofs")
|
||||
.WithTags("Provcache Evidence")
|
||||
.WithOpenApi();
|
||||
|
||||
// GET /v1/provcache/proofs/{proofRoot}
|
||||
proofsGroup.MapGet("/{proofRoot}", GetEvidenceChunks)
|
||||
.WithName("GetProofEvidence")
|
||||
.WithSummary("Get evidence chunks by proof root")
|
||||
.WithDescription("Retrieves evidence chunks for a proof root with pagination support. Use cursor parameter for subsequent pages.")
|
||||
.Produces<ProofEvidenceResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
|
||||
// GET /v1/provcache/proofs/{proofRoot}/manifest
|
||||
proofsGroup.MapGet("/{proofRoot}/manifest", GetProofManifest)
|
||||
.WithName("GetProofManifest")
|
||||
.WithSummary("Get chunk manifest (metadata without data)")
|
||||
.WithDescription("Retrieves the chunk manifest for lazy evidence fetching. Contains hashes and sizes but no blob data.")
|
||||
.Produces<ProofManifestResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
|
||||
// GET /v1/provcache/proofs/{proofRoot}/chunks/{chunkIndex}
|
||||
proofsGroup.MapGet("/{proofRoot}/chunks/{chunkIndex:int}", GetSingleChunk)
|
||||
.WithName("GetProofChunk")
|
||||
.WithSummary("Get a single chunk by index")
|
||||
.WithDescription("Retrieves a specific chunk by its index within the proof.")
|
||||
.Produces<ProofChunkResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
|
||||
// POST /v1/provcache/proofs/{proofRoot}/verify
|
||||
proofsGroup.MapPost("/{proofRoot}/verify", VerifyProof)
|
||||
.WithName("VerifyProof")
|
||||
.WithSummary("Verify Merkle tree integrity")
|
||||
.WithDescription("Verifies all chunk hashes and the Merkle tree for the proof root.")
|
||||
.Produces<ProofVerificationResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
@@ -278,3 +320,234 @@ internal sealed class ProblemDetails
|
||||
public string? Detail { get; set; }
|
||||
public string? Instance { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marker class for logging in Proofs API endpoints.
|
||||
/// </summary>
|
||||
public sealed class ProofsApiEndpoints;
|
||||
|
||||
partial class ProvcacheEndpointExtensions
|
||||
{
|
||||
private const int DefaultPageSize = 10;
|
||||
private const int MaxPageSize = 100;
|
||||
|
||||
/// <summary>
|
||||
/// GET /v1/provcache/proofs/{proofRoot}
|
||||
/// </summary>
|
||||
private static async Task<IResult> GetEvidenceChunks(
|
||||
string proofRoot,
|
||||
int? offset,
|
||||
int? limit,
|
||||
bool? includeData,
|
||||
[FromServices] IEvidenceChunkRepository chunkRepository,
|
||||
ILogger<ProofsApiEndpoints> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("GET /v1/provcache/proofs/{ProofRoot} offset={Offset} limit={Limit}", proofRoot, offset, limit);
|
||||
|
||||
try
|
||||
{
|
||||
var startIndex = offset ?? 0;
|
||||
var pageSize = Math.Min(limit ?? DefaultPageSize, MaxPageSize);
|
||||
|
||||
// Get manifest for total count
|
||||
var manifest = await chunkRepository.GetManifestAsync(proofRoot, cancellationToken);
|
||||
if (manifest is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
// Get chunk range
|
||||
var chunks = await chunkRepository.GetChunkRangeAsync(proofRoot, startIndex, pageSize, cancellationToken);
|
||||
|
||||
var chunkResponses = chunks.Select(c => new ProofChunkResponse
|
||||
{
|
||||
ChunkId = c.ChunkId,
|
||||
Index = c.ChunkIndex,
|
||||
Hash = c.ChunkHash,
|
||||
Size = c.BlobSize,
|
||||
ContentType = c.ContentType,
|
||||
Data = includeData == true ? Convert.ToBase64String(c.Blob) : null
|
||||
}).ToList();
|
||||
|
||||
var hasMore = startIndex + chunks.Count < manifest.TotalChunks;
|
||||
var nextCursor = hasMore ? (startIndex + pageSize).ToString() : null;
|
||||
|
||||
return Results.Ok(new ProofEvidenceResponse
|
||||
{
|
||||
ProofRoot = proofRoot,
|
||||
TotalChunks = manifest.TotalChunks,
|
||||
TotalSize = manifest.TotalSize,
|
||||
Chunks = chunkResponses,
|
||||
NextCursor = nextCursor,
|
||||
HasMore = hasMore
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error getting evidence chunks for proof root {ProofRoot}", proofRoot);
|
||||
return Results.Problem(
|
||||
detail: ex.Message,
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
title: "Evidence retrieval failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GET /v1/provcache/proofs/{proofRoot}/manifest
|
||||
/// </summary>
|
||||
private static async Task<IResult> GetProofManifest(
|
||||
string proofRoot,
|
||||
[FromServices] IEvidenceChunkRepository chunkRepository,
|
||||
ILogger<ProofsApiEndpoints> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("GET /v1/provcache/proofs/{ProofRoot}/manifest", proofRoot);
|
||||
|
||||
try
|
||||
{
|
||||
var manifest = await chunkRepository.GetManifestAsync(proofRoot, cancellationToken);
|
||||
if (manifest is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var chunkMetadata = manifest.Chunks.Select(c => new ChunkMetadataResponse
|
||||
{
|
||||
ChunkId = c.ChunkId,
|
||||
Index = c.Index,
|
||||
Hash = c.Hash,
|
||||
Size = c.Size,
|
||||
ContentType = c.ContentType
|
||||
}).ToList();
|
||||
|
||||
return Results.Ok(new ProofManifestResponse
|
||||
{
|
||||
ProofRoot = proofRoot,
|
||||
TotalChunks = manifest.TotalChunks,
|
||||
TotalSize = manifest.TotalSize,
|
||||
Chunks = chunkMetadata,
|
||||
GeneratedAt = manifest.GeneratedAt
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error getting manifest for proof root {ProofRoot}", proofRoot);
|
||||
return Results.Problem(
|
||||
detail: ex.Message,
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
title: "Manifest retrieval failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GET /v1/provcache/proofs/{proofRoot}/chunks/{chunkIndex}
|
||||
/// </summary>
|
||||
private static async Task<IResult> GetSingleChunk(
|
||||
string proofRoot,
|
||||
int chunkIndex,
|
||||
[FromServices] IEvidenceChunkRepository chunkRepository,
|
||||
ILogger<ProofsApiEndpoints> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("GET /v1/provcache/proofs/{ProofRoot}/chunks/{ChunkIndex}", proofRoot, chunkIndex);
|
||||
|
||||
try
|
||||
{
|
||||
var chunk = await chunkRepository.GetChunkAsync(proofRoot, chunkIndex, cancellationToken);
|
||||
if (chunk is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
return Results.Ok(new ProofChunkResponse
|
||||
{
|
||||
ChunkId = chunk.ChunkId,
|
||||
Index = chunk.ChunkIndex,
|
||||
Hash = chunk.ChunkHash,
|
||||
Size = chunk.BlobSize,
|
||||
ContentType = chunk.ContentType,
|
||||
Data = Convert.ToBase64String(chunk.Blob)
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error getting chunk {ChunkIndex} for proof root {ProofRoot}", chunkIndex, proofRoot);
|
||||
return Results.Problem(
|
||||
detail: ex.Message,
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
title: "Chunk retrieval failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// POST /v1/provcache/proofs/{proofRoot}/verify
|
||||
/// </summary>
|
||||
private static async Task<IResult> VerifyProof(
|
||||
string proofRoot,
|
||||
[FromServices] IEvidenceChunkRepository chunkRepository,
|
||||
[FromServices] IEvidenceChunker chunker,
|
||||
ILogger<ProofsApiEndpoints> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("POST /v1/provcache/proofs/{ProofRoot}/verify", proofRoot);
|
||||
|
||||
try
|
||||
{
|
||||
var chunks = await chunkRepository.GetChunksAsync(proofRoot, cancellationToken);
|
||||
if (chunks.Count == 0)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var chunkResults = new List<ChunkVerificationResult>();
|
||||
var allValid = true;
|
||||
|
||||
foreach (var chunk in chunks)
|
||||
{
|
||||
var isValid = chunker.VerifyChunk(chunk);
|
||||
var computedHash = isValid ? chunk.ChunkHash : ComputeChunkHash(chunk.Blob);
|
||||
|
||||
chunkResults.Add(new ChunkVerificationResult
|
||||
{
|
||||
Index = chunk.ChunkIndex,
|
||||
IsValid = isValid,
|
||||
ExpectedHash = chunk.ChunkHash,
|
||||
ComputedHash = isValid ? null : computedHash
|
||||
});
|
||||
|
||||
if (!isValid)
|
||||
{
|
||||
allValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify Merkle root
|
||||
var chunkHashes = chunks.Select(c => c.ChunkHash).ToList();
|
||||
var computedRoot = chunker.ComputeMerkleRoot(chunkHashes);
|
||||
var rootMatches = string.Equals(computedRoot, proofRoot, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return Results.Ok(new ProofVerificationResponse
|
||||
{
|
||||
ProofRoot = proofRoot,
|
||||
IsValid = allValid && rootMatches,
|
||||
ChunkResults = chunkResults,
|
||||
Error = !rootMatches ? $"Merkle root mismatch. Expected: {proofRoot}, Computed: {computedRoot}" : null
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error verifying proof root {ProofRoot}", proofRoot);
|
||||
return Results.Problem(
|
||||
detail: ex.Message,
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
title: "Proof verification failed");
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeChunkHash(byte[] data)
|
||||
{
|
||||
var hash = System.Security.Cryptography.SHA256.HashData(data);
|
||||
return $"sha256:{Convert.ToHexStringLower(hash)}";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user