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.
175 lines
6.8 KiB
175 lines
6.8 KiB
package drand_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
"g.activething.com/go/DateKeys/internal/testkit"
|
|
"g.activething.com/go/DateKeys/profile"
|
|
"g.activething.com/go/DateKeys/provider"
|
|
"g.activething.com/go/DateKeys/provider/drand"
|
|
)
|
|
|
|
var sig1000 = hex.EncodeToString(testkit.Release(1000).Signature)
|
|
|
|
func serve(t *testing.T, h http.HandlerFunc) string {
|
|
t.Helper()
|
|
s := httptest.NewServer(h)
|
|
t.Cleanup(s.Close)
|
|
return s.URL
|
|
}
|
|
|
|
func TestRaceWaitsForAValidSignature(t *testing.T) {
|
|
p := profile.Quicknet()
|
|
bad := serve(t, func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprintf(w, `{"round":1000,"signature":"%s"}`, strings.Repeat("0", 96))
|
|
})
|
|
good := serve(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/v2/chains/"+p.ChainHashHex()+"/rounds/1000" {
|
|
t.Errorf("request not pinned to the chain: %s", r.URL.Path)
|
|
}
|
|
time.Sleep(25 * time.Millisecond)
|
|
fmt.Fprintf(w, `{"round":1000,"signature":"%s"}`, sig1000)
|
|
})
|
|
c := drand.NewWithHTTPClient(http.DefaultClient, bad, good)
|
|
rel, err := c.Fetch(context.Background(), p, provider.Condition{Round: 1000})
|
|
if err != nil || hex.EncodeToString(rel.Signature) != sig1000 || rel.Round != 1000 {
|
|
t.Fatalf("race failed: %+v %v", rel, err)
|
|
}
|
|
}
|
|
|
|
func TestRejectMalformedRelayResponses(t *testing.T) {
|
|
p := profile.Quicknet()
|
|
for _, payload := range []string{
|
|
`{"round":999,"signature":"` + sig1000 + `"}`,
|
|
`{"round":1000,"signature":"` + sig1000 + `","randomness":"fake"}`,
|
|
`{"round":1000,"signature":"fake"}`,
|
|
`{"round":1000,"signature":"` + sig1000 + `"} {}`,
|
|
`{"round":1000,"signature":"` + hex.EncodeToString(testkit.Release(1001).Signature) + `"}`,
|
|
strings.Repeat("x", 9000),
|
|
``,
|
|
} {
|
|
url := serve(t, func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, payload) })
|
|
c := drand.NewWithHTTPClient(http.DefaultClient, url)
|
|
_, err := c.Fetch(context.Background(), p, provider.Condition{Round: 1000})
|
|
if !onlyUnavailable(err) {
|
|
t.Errorf("accepted or misclassified %.40q: %v", payload, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// onlyUnavailable reports whether err wraps ErrReleaseUnavailable and no
|
|
// other normative error: every error of this module wraps exactly one.
|
|
func onlyUnavailable(err error) bool {
|
|
for _, e := range datekeys.All() {
|
|
if errors.Is(err, e) != (e == datekeys.ErrReleaseUnavailable) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Spec §63 step 9: when no relay returns a valid release, the only code of
|
|
// the error of Fetch is ErrReleaseUnavailable, although a relay's answer
|
|
// breaks a rule of step 10, whose code its failure carries: a release of
|
|
// another round, signed for that round, or a negated signature. The failure
|
|
// of each relay stays in the text.
|
|
func TestFetchErrorHasOneCode(t *testing.T) {
|
|
p := profile.Quicknet()
|
|
other := serve(t, func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprintf(w, `{"round":1001,"signature":"%s"}`, hex.EncodeToString(testkit.Release(1001).Signature))
|
|
})
|
|
negated := serve(t, func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprintf(w, `{"round":1000,"signature":"%s"}`, hex.EncodeToString(testkit.Negated(testkit.Release(1000).Signature)))
|
|
})
|
|
reason := map[string]string{
|
|
other: "release for round 1001, expected 1000: ERR_ROUND_MISMATCH",
|
|
negated: "does not verify as the BLS signature of round 1000",
|
|
}
|
|
for _, relays := range [][]string{{other}, {other, negated}} {
|
|
_, err := drand.NewWithHTTPClient(http.DefaultClient, relays...).Fetch(context.Background(), p, provider.Condition{Round: 1000})
|
|
if err == nil {
|
|
t.Fatalf("%d relays: a release was accepted", len(relays))
|
|
}
|
|
if !onlyUnavailable(err) || datekeys.Code(err) != "ERR_RELEASE_UNAVAILABLE" {
|
|
t.Errorf("%d relays: %v wraps another normative error", len(relays), err)
|
|
}
|
|
for _, relay := range relays {
|
|
if !strings.Contains(err.Error(), relay+": provider: ") || !strings.Contains(err.Error(), reason[relay]) {
|
|
t.Errorf("%d relays: the failure of %s is not in %q", len(relays), relay, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRandomnessMustMatchWhenPresent(t *testing.T) {
|
|
p := profile.Quicknet()
|
|
sum := "e1f1cb5a9ddd0e2ae4a4a8c5bf42e8e1e1ed8b5a9f1f7da1a3f1d2bb0f1b2c3d"
|
|
url := serve(t, func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprintf(w, `{"round":1000,"signature":"%s","randomness":"%s"}`, sig1000, sum)
|
|
})
|
|
_, err := drand.NewWithHTTPClient(http.DefaultClient, url).Fetch(context.Background(), p, provider.Condition{Round: 1000})
|
|
if !onlyUnavailable(err) || !strings.Contains(err.Error(), "randomness does not match the signature") {
|
|
t.Fatalf("wrong randomness accepted: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestUnavailabilityAndCancellation(t *testing.T) {
|
|
p := profile.Quicknet()
|
|
for _, status := range []int{404, 425, 503} {
|
|
url := serve(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(status) })
|
|
_, err := drand.NewWithHTTPClient(http.DefaultClient, url).Fetch(context.Background(), p, provider.Condition{Round: 1000})
|
|
if !onlyUnavailable(err) {
|
|
t.Fatalf("HTTP %d: %v", status, err)
|
|
}
|
|
}
|
|
// A context that ends before the request, or during it, stays
|
|
// detectable, whether Fetch sees it end before the relay fails because
|
|
// of it or after.
|
|
url := serve(t, func(w http.ResponseWriter, r *http.Request) { time.Sleep(time.Second) })
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
if _, err := drand.NewWithHTTPClient(http.DefaultClient, url).Fetch(ctx, p, provider.Condition{Round: 1000}); !onlyUnavailable(err) || !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("ignored cancellation: %v", err)
|
|
}
|
|
ctx, cancel = context.WithTimeout(context.Background(), 20*time.Millisecond)
|
|
defer cancel()
|
|
if _, err := drand.NewWithHTTPClient(http.DefaultClient, url).Fetch(ctx, p, provider.Condition{Round: 1000}); !onlyUnavailable(err) || !errors.Is(err, context.DeadlineExceeded) {
|
|
t.Fatalf("ignored the deadline: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRedirectsAreNotFollowed(t *testing.T) {
|
|
p := profile.Quicknet()
|
|
target := serve(t, func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprintf(w, `{"round":1000,"signature":"%s"}`, sig1000)
|
|
})
|
|
redirect := serve(t, func(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, target+r.URL.Path, http.StatusFound)
|
|
})
|
|
if _, err := drand.New(redirect).Fetch(context.Background(), p, provider.Condition{Round: 1000}); err == nil {
|
|
t.Fatal("followed a redirect")
|
|
}
|
|
}
|
|
|
|
func TestInvalidRequests(t *testing.T) {
|
|
c := drand.New("http://127.0.0.1:1")
|
|
p := profile.Quicknet()
|
|
if _, err := c.Fetch(context.Background(), p, provider.Condition{Round: 0}); !errors.Is(err, datekeys.ErrDateKeyInvalid) {
|
|
t.Fatalf("round 0: %v", err)
|
|
}
|
|
other := profile.Quicknet()
|
|
other.Provider = "other"
|
|
if _, err := c.Fetch(context.Background(), other, provider.Condition{Round: 1}); !errors.Is(err, datekeys.ErrUnknownProfile) {
|
|
t.Fatalf("non-drand profile: %v", err)
|
|
}
|
|
}
|