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:
parent
e557d85d5a
commit
f897e80c95
3 changed files with 120 additions and 15 deletions
|
|
@ -158,3 +158,32 @@ func (c *Client) GetCertificate(ctx context.Context, hash string) (*Certificate,
|
|||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// CertChain walks previous_hash backwards from headHash until it hits the
|
||||
// first certificate that carries a genesis_signature. Returns the chain
|
||||
// ordered head-first. Caller can invert if root-first is preferred.
|
||||
//
|
||||
// The chain length is usually 1-3 certs per epoch boundary; an unbounded
|
||||
// walk would be a footgun so it caps at maxDepth.
|
||||
func (c *Client) CertChain(ctx context.Context, headHash string, maxDepth int) ([]*Certificate, error) {
|
||||
if maxDepth <= 0 {
|
||||
maxDepth = 1024
|
||||
}
|
||||
var chain []*Certificate
|
||||
next := headHash
|
||||
for i := 0; i < maxDepth; i++ {
|
||||
if next == "" {
|
||||
return nil, fmt.Errorf("chain broke at depth %d: no previous_hash", i)
|
||||
}
|
||||
cert, err := c.GetCertificate(ctx, next)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("depth %d (%s): %w", i, next, err)
|
||||
}
|
||||
chain = append(chain, cert)
|
||||
if cert.GenesisSignature != "" {
|
||||
return chain, nil
|
||||
}
|
||||
next = cert.PreviousHash
|
||||
}
|
||||
return nil, fmt.Errorf("cert chain exceeded max depth %d without reaching genesis", maxDepth)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue