certificate chain walker + progress bar fix

- aggregator.CertChain: walks previous_hash from head until genesis_signature
- cmd: 'cert' subcommand, -chain flag for full walk, 'head' shortcut resolves
  latest snapshot's certificate_hash
- ProgressFn now signals both bytes-read and total-from-Content-Length so
  percent is computed against the actual transfer size, not the uncompressed
  target
- verified against preprod: 90-cert chain head→genesis, Ed25519 genesis cert
  shape (64-byte sig over 32-byte signed_message, protocol_message carries
  next_aggregate_verification_key for BLS), STM-signed non-genesis certs

pipeline is now verification-sprint ready
This commit is contained in:
Sulkta 2026-04-23 15:20:32 -07:00
parent e4012bf79a
commit 517df1130d
3 changed files with 120 additions and 15 deletions

View file

@ -14,6 +14,10 @@ import (
"time"
)
// ProgressFn is called with (bytesRead, totalBytes) where totalBytes is
// taken from the response Content-Length (0 if unknown — server didn't send).
type ProgressFn func(read, total int64)
// Download fetches a URL to destPath, resuming from a .part file if one
// exists. If expectedSHA256 is non-empty, the final file is integrity-checked.
// Progress is reported via the supplied callback (called with current bytes).
@ -25,7 +29,7 @@ import (
// - Resume is implemented via the HTTP Range header against the existing
// .part file size; falls back to full download if the server refuses.
// - destPath is atomically replaced only after SHA validation passes.
func Download(ctx context.Context, uri, destPath, expectedSHA256 string, progress func(bytes int64)) error {
func Download(ctx context.Context, uri, destPath, expectedSHA256 string, progress ProgressFn) error {
if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil {
return fmt.Errorf("mkdir: %w", err)
}
@ -77,8 +81,15 @@ func Download(ctx context.Context, uri, destPath, expectedSHA256 string, progres
}
}
// totalBytes = existing + response.ContentLength if server sent one;
// for resumed partial responses Content-Length is the remaining, not total.
var totalSize int64
if resp.ContentLength > 0 {
totalSize = existing + resp.ContentLength
}
w := io.MultiWriter(out, h)
total := existing
read := existing
buf := make([]byte, 256*1024)
lastProgress := time.Now()
for {
@ -87,9 +98,9 @@ func Download(ctx context.Context, uri, destPath, expectedSHA256 string, progres
if _, werr := w.Write(buf[:n]); werr != nil {
return fmt.Errorf("write: %w", werr)
}
total += int64(n)
read += int64(n)
if progress != nil && time.Since(lastProgress) > 250*time.Millisecond {
progress(total)
progress(read, totalSize)
lastProgress = time.Now()
}
}
@ -101,7 +112,7 @@ func Download(ctx context.Context, uri, destPath, expectedSHA256 string, progres
}
}
if progress != nil {
progress(total)
progress(read, totalSize)
}
if err := out.Close(); err != nil {
return err
@ -120,7 +131,7 @@ func Download(ctx context.Context, uri, destPath, expectedSHA256 string, progres
var ErrNoLocations = errors.New("no download locations available")
// DownloadFirst tries each URI in order until one succeeds.
func DownloadFirst(ctx context.Context, uris []string, destPath, expectedSHA256 string, progress func(int64)) error {
func DownloadFirst(ctx context.Context, uris []string, destPath, expectedSHA256 string, progress ProgressFn) error {
if len(uris) == 0 {
return ErrNoLocations
}