You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/provider/drand/client.go

157 lines
5.3 KiB

// Package drand fetches Quicknet releases directly from public drand relays
// (spec §48, §49). HTTP is an untrusted transport: every response is verified
// locally with provider.Verify against the pinned profile before it is
// returned, and authenticity comes from the BLS signature, never from the
Spec v0.8.2: corrections from the formal review A formal review of the whole v0.8.2 text found it approvable after these corrections, recorded in §76 ("Correcciones de la revisión formal"): - §27 no longer calls header_binding the authenticity of PUBLIC_HEADER: it binds the header to the opened control, never authorship or date (§55.1); the age MAC only protects against whoever lacks the file key. - §63 steps 9 and 10: a network source (relay, Release API, cache) MUST verify every response and gives ERR_RELEASE_UNAVAILABLE at step 9 when none verifies; the step-10 codes are for a directly supplied release. The reference already behaved so; TestReleaseFromANetworkSource pins both paths. - §54 and §72: registrations declare the objects and arrays where an extension may appear, and a known extension out of place counts as unknown there. The reference gains the optional extension.Placement interface, used at steps 4, 9.a and 14. - §63 step 11 fixes the GT serialization hashed by H2 (kilic/kyber order) with the frozen vector H2(e(G1, G2))[:16] = cb87319f..., shared as testdata/vectors/tlock_ibe.json; H2-H4 are cited to drand/kyber. - Step 5 makes the SEALED_CONTROL read mandatory, step 15 names ERR_HEADER_BINDING, §21 makes capsule_id 16 CSPRNG bytes a MUST, §76 is made accurate (four dk1.json vectors, the §36 time_only rule, two cases rewritten against the texts that really existed), and editorial fixes in §5, §36, §55.1, §69.1 and §77. §73 lists the three new decisions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
// hostname (spec §48, §52). A response that fails is discarded; when no relay
Spec v0.8.2: second-round corrections from the formal review The second round of the formal review confirmed the nine corrections of c57ed48 and asked for these, recorded in §76 as corrections 4 to 6 and an editorial note: - §72: an encoder MUST NOT write a registered extension in an object or array it is not registered for; §54: a reader MUST NOT interpret the data of a noncritical one it ignores for that reason. capsule.Encrypt and accesskey.Encode take no Registry, so the application applies the rule; their documentation and extension.Placement say so. - §17 and §51 give the step-10 codes only for a directly supplied release, as step 10 does; a network source discards a failing one at step 9. - Step 9 reports ERR_RELEASE_UNAVAILABLE and no other code, whatever the failure of the source. provider/drand.Client keeps each relay's failure as text only (errors.Join made a relay's ERR_ROUND_MISMATCH match with errors.Is), and capsule.Open keeps only the text of a source error that carries another code (a caller's source failing with ERR_RELEASE_INVALID gave that code at step 9). A context that ended stays detectable: Fetch now has a single failure path, so the canceled and deadline cases are deterministic. - TestExtensionPlacement covers the noncritical array of a .dkk: with the object-blind extension.CheckNoncritical at step 9.a it fails. - Editorial: §28.1 "analizan solo la cabecera age", one arrow at step 9, two §76 introductions; §73 lines for release sources and placement. - testdata/README.md says the corpus registers its extensions in both arrays of every object; traceability, CHANGELOG and both READMEs (integrity holds against whoever lacks the file keys, §27, §55.1) follow. Spec dated 28 September 2026; new SHA-256 in spec/README.md. No fixture or vector changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
// returns a valid release, the error of Fetch wraps
// datekeys.ErrReleaseUnavailable, the code of spec §63 step 9, and no other
// normative error: the failure of each relay is kept in its text only, for
// diagnosis.
package drand
import (
"context"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/profile"
"g.activething.com/go/DateKeys/provider"
)
// Limits of a single relay exchange.
const (
DefaultTimeout = 6 * time.Second
maxResponseSize = 8 << 10
)
// DefaultRelays returns the public drand relays used when none are given.
func DefaultRelays() []string {
return []string{"https://api.drand.sh", "https://api2.drand.sh", "https://api3.drand.sh"}
}
// Client races independent relays and returns the first release that passes
// local verification. It implements provider.ReleaseSource.
type Client struct {
http *http.Client
relays []string
timeout time.Duration
}
var _ provider.ReleaseSource = (*Client)(nil)
// New returns a client for the given relay base URLs, or DefaultRelays.
// Redirects are not followed.
func New(relays ...string) *Client {
return NewWithHTTPClient(&http.Client{
Timeout: DefaultTimeout,
CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
}, relays...)
}
// NewWithHTTPClient is New with a caller-supplied HTTP client.
func NewWithHTTPClient(hc *http.Client, relays ...string) *Client {
if len(relays) == 0 {
relays = DefaultRelays()
}
return &Client{http: hc, relays: append([]string(nil), relays...), timeout: DefaultTimeout}
}
// Fetch implements provider.ReleaseSource. Only a cryptographically valid
// release for exactly the requested round wins the race.
func (c *Client) Fetch(ctx context.Context, p *profile.Profile, cond provider.Condition) (provider.Release, error) {
if p.Provider != profile.ProviderDrand {
return provider.Release{}, fmt.Errorf("drand: profile %s is not a drand profile: %w", p.ID, datekeys.ErrUnknownProfile)
}
if cond.Round == 0 || cond.Round > p.MaxRound() {
return provider.Release{}, fmt.Errorf("drand: round %d outside the range of %s: %w", cond.Round, p.ID, datekeys.ErrDateKeyInvalid)
}
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
type result struct {
release provider.Release
err error
}
ch := make(chan result, len(c.relays))
for _, relay := range c.relays {
go func(relay string) {
r, err := c.fetch(ctx, relay, p, cond)
ch <- result{r, err}
}(relay)
}
var failures []error
Spec v0.8.2: second-round corrections from the formal review The second round of the formal review confirmed the nine corrections of c57ed48 and asked for these, recorded in §76 as corrections 4 to 6 and an editorial note: - §72: an encoder MUST NOT write a registered extension in an object or array it is not registered for; §54: a reader MUST NOT interpret the data of a noncritical one it ignores for that reason. capsule.Encrypt and accesskey.Encode take no Registry, so the application applies the rule; their documentation and extension.Placement say so. - §17 and §51 give the step-10 codes only for a directly supplied release, as step 10 does; a network source discards a failing one at step 9. - Step 9 reports ERR_RELEASE_UNAVAILABLE and no other code, whatever the failure of the source. provider/drand.Client keeps each relay's failure as text only (errors.Join made a relay's ERR_ROUND_MISMATCH match with errors.Is), and capsule.Open keeps only the text of a source error that carries another code (a caller's source failing with ERR_RELEASE_INVALID gave that code at step 9). A context that ended stays detectable: Fetch now has a single failure path, so the canceled and deadline cases are deterministic. - TestExtensionPlacement covers the noncritical array of a .dkk: with the object-blind extension.CheckNoncritical at step 9.a it fails. - Editorial: §28.1 "analizan solo la cabecera age", one arrow at step 9, two §76 introductions; §73 lines for release sources and placement. - testdata/README.md says the corpus registers its extensions in both arrays of every object; traceability, CHANGELOG and both READMEs (integrity holds against whoever lacks the file keys, §27, §55.1) follow. Spec dated 28 September 2026; new SHA-256 in spec/README.md. No fixture or vector changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
wait:
for range c.relays {
select {
case <-ctx.Done():
Spec v0.8.2: second-round corrections from the formal review The second round of the formal review confirmed the nine corrections of c57ed48 and asked for these, recorded in §76 as corrections 4 to 6 and an editorial note: - §72: an encoder MUST NOT write a registered extension in an object or array it is not registered for; §54: a reader MUST NOT interpret the data of a noncritical one it ignores for that reason. capsule.Encrypt and accesskey.Encode take no Registry, so the application applies the rule; their documentation and extension.Placement say so. - §17 and §51 give the step-10 codes only for a directly supplied release, as step 10 does; a network source discards a failing one at step 9. - Step 9 reports ERR_RELEASE_UNAVAILABLE and no other code, whatever the failure of the source. provider/drand.Client keeps each relay's failure as text only (errors.Join made a relay's ERR_ROUND_MISMATCH match with errors.Is), and capsule.Open keeps only the text of a source error that carries another code (a caller's source failing with ERR_RELEASE_INVALID gave that code at step 9). A context that ended stays detectable: Fetch now has a single failure path, so the canceled and deadline cases are deterministic. - TestExtensionPlacement covers the noncritical array of a .dkk: with the object-blind extension.CheckNoncritical at step 9.a it fails. - Editorial: §28.1 "analizan solo la cabecera age", one arrow at step 9, two §76 introductions; §73 lines for release sources and placement. - testdata/README.md says the corpus registers its extensions in both arrays of every object; traceability, CHANGELOG and both READMEs (integrity holds against whoever lacks the file keys, §27, §55.1) follow. Spec dated 28 September 2026; new SHA-256 in spec/README.md. No fixture or vector changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
break wait
case r := <-ch:
if r.err == nil {
return r.release, nil
}
failures = append(failures, r.err)
}
}
Spec v0.8.2: second-round corrections from the formal review The second round of the formal review confirmed the nine corrections of c57ed48 and asked for these, recorded in §76 as corrections 4 to 6 and an editorial note: - §72: an encoder MUST NOT write a registered extension in an object or array it is not registered for; §54: a reader MUST NOT interpret the data of a noncritical one it ignores for that reason. capsule.Encrypt and accesskey.Encode take no Registry, so the application applies the rule; their documentation and extension.Placement say so. - §17 and §51 give the step-10 codes only for a directly supplied release, as step 10 does; a network source discards a failing one at step 9. - Step 9 reports ERR_RELEASE_UNAVAILABLE and no other code, whatever the failure of the source. provider/drand.Client keeps each relay's failure as text only (errors.Join made a relay's ERR_ROUND_MISMATCH match with errors.Is), and capsule.Open keeps only the text of a source error that carries another code (a caller's source failing with ERR_RELEASE_INVALID gave that code at step 9). A context that ended stays detectable: Fetch now has a single failure path, so the canceled and deadline cases are deterministic. - TestExtensionPlacement covers the noncritical array of a .dkk: with the object-blind extension.CheckNoncritical at step 9.a it fails. - Editorial: §28.1 "analizan solo la cabecera age", one arrow at step 9, two §76 introductions; §73 lines for release sources and placement. - testdata/README.md says the corpus registers its extensions in both arrays of every object; traceability, CHANGELOG and both READMEs (integrity holds against whoever lacks the file keys, §27, §55.1) follow. Spec dated 28 September 2026; new SHA-256 in spec/README.md. No fixture or vector changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
err := fmt.Errorf("drand: no relay returned a verified release for round %d%s: %w",
cond.Round, reasons(failures), datekeys.ErrReleaseUnavailable)
if ctx.Err() != nil {
// The context ended, before or after the relays failed because of
// it: that stays detectable with errors.Is.
return provider.Release{}, fmt.Errorf("%w: %w", err, ctx.Err())
}
return provider.Release{}, err
}
// reasons keeps the failure of each relay as text only. A relay's answer may
// break a rule of spec §63 step 10, whose code its error wraps, but Fetch
// reports ErrReleaseUnavailable alone: every error of this module wraps
// exactly one normative code.
func reasons(failures []error) string {
if len(failures) == 0 {
return ""
}
s := make([]string, len(failures))
for i, err := range failures {
s[i] = err.Error()
}
return ": " + strings.Join(s, "; ")
}
func (c *Client) fetch(ctx context.Context, relay string, p *profile.Profile, cond provider.Condition) (provider.Release, error) {
url := strings.TrimRight(relay, "/") + "/v2/chains/" + p.ChainHashHex() + "/rounds/" + strconv.FormatUint(cond.Round, 10)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return provider.Release{}, fmt.Errorf("%s: %w", relay, err)
}
req.Header.Set("Accept", "application/json")
res, err := c.http.Do(req)
if err != nil {
return provider.Release{}, fmt.Errorf("%s: %w", relay, err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return provider.Release{}, fmt.Errorf("%s: HTTP %d", relay, res.StatusCode)
}
b, err := io.ReadAll(io.LimitReader(res.Body, maxResponseSize+1))
if err != nil {
return provider.Release{}, fmt.Errorf("%s: %w", relay, err)
}
if len(b) > maxResponseSize {
return provider.Release{}, fmt.Errorf("%s: response larger than %d bytes: %w", relay, maxResponseSize, datekeys.ErrReleaseInvalid)
}
// The answer is read as a release the caller gives, with the strict
// rules of spec v0.16, §47.1, randomness included.
release, err := provider.ParseDrandJSON(b)
if err != nil {
return provider.Release{}, fmt.Errorf("%s: %w", relay, err)
}
if err := provider.Verify(p, cond, release); err != nil {
return provider.Release{}, fmt.Errorf("%s: %w", relay, err)
}
return release, nil
}

Powered by TurnKey Linux.