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/release_test.go

188 lines
5.7 KiB

Release object, release in hand and step 9.c option B (spec v0.15 draft) The release of a round becomes a file, .dkr: a release object in deterministic CBOR, {0: "datekeys-release", 1: 1, 2: chain_hash, 3: round, 4: signature}, which provider.EncodeRelease writes and DecodeRelease reads with its layers (size, type and version, schema). provider.ParseRelease also reads drand's JSON as the input of the caller. Verify checks the chain hash a release names before its round and its signature, with ERR_PROFILE_MISMATCH. provider.Archive reads a local release archive, the informative format of the draft. capsule.OpenOptions.Release takes a release in hand, a provider.Supplier, exclusive with Source: Open does not compare it with the clock (step 9.c, option B) and reports a clock behind it in Opened.ClockBehind; a network source is still never asked before the round time. The CLI gains decrypt -release FILE (.dkr, drand's JSON or a local archive), decrypt -save-release FILE.dkr and the command release, which fetches, verifies and saves the .dkr without opening the capsule. Test data: vectors/release.json, releases/<round>.dkr for rounds 1000, 1001, 1004 and 2000, and a local archive of rounds 1000 to 1004. In mutations.json every case says its source, "supplied" or "network"; the case "round not reached yet", a release in hand, now opens, and four cases are added: the same with a network source, a release of another round from a network source, and two release objects of another chain. SpecVersion stays 0.14 until the author approves the draft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
package provider_test
import (
"bytes"
"encoding/hex"
"errors"
"os"
"testing"
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"
)
// Spec v0.15, §47.1: every vector of release.json, replayed from the file
// alone, gets its result and its text: decoding, then step 10 against the
// pinned profile and the round of the vector.
func TestReleaseVectors(t *testing.T) {
var f testkit.ReleaseVectorFile
if err := testkit.ReadJSON("../testdata/vectors/release.json", &f); err != nil {
t.Fatal(err)
}
p := profile.Quicknet()
check := func(t *testing.T, in []byte, v testkit.ReleaseVector) {
r, err := provider.ParseRelease(in)
if err == nil {
err = provider.Verify(p, provider.Condition{Round: v.Round}, r)
}
if got := testkit.Result(err); got != v.Result {
t.Fatalf("%s, want %s (%v)", got, v.Result, err)
}
if err != nil && err.Error() != v.Text {
t.Fatalf("text %q, want %q", err.Error(), v.Text)
}
}
for _, v := range f.Objects {
t.Run(v.Name, func(t *testing.T) {
b, err := hex.DecodeString(v.Encoding)
if err != nil {
t.Fatal(err)
}
check(t, b, v)
if v.Result == testkit.ResultOK {
// A valid object is the deterministic encoding of its value.
r, _ := provider.DecodeRelease(b)
if again, err := provider.EncodeRelease(r); err != nil || !bytes.Equal(again, b) {
t.Fatalf("encoding again: %x, %v", again, err)
}
}
})
}
for _, v := range f.JSON {
t.Run("JSON "+v.Name, func(t *testing.T) { check(t, []byte(v.Input), v) })
}
if len(f.Objects) < 30 || len(f.JSON) < 10 {
t.Fatalf("%d objects and %d JSON inputs", len(f.Objects), len(f.JSON))
}
}
// The local archive of testdata supplies the .dkr of each round it holds,
// and nothing for a round it lacks or outside it, for another chain, or when
// its length is not the one its header announces.
func TestArchive(t *testing.T) {
b, err := os.ReadFile("../testdata/releases/" + testkit.ArchiveFile)
if err != nil {
t.Fatal(err)
}
if !provider.IsArchive(b) || provider.IsArchive(mustRead(t, "../testdata/releases/1000.dkr")) {
t.Fatal("IsArchive")
}
p := profile.Quicknet()
a := provider.NewArchive(bytes.NewReader(b), int64(len(b)))
for _, round := range []uint64{1000, 1001, 1004} {
got, err := a.Supply(p, provider.Condition{Round: round})
if err != nil || !bytes.Equal(got, mustRead(t, "../testdata/releases/"+testkit.ReleaseFileName(round))) {
t.Fatalf("round %d: %x, %v", round, got, err)
}
}
for _, round := range []uint64{999, 1002, 1003, 1005, 2000} {
if _, err := a.Supply(p, provider.Condition{Round: round}); !onlyUnavailable(err) {
t.Fatalf("round %d: %v", round, err)
}
}
other := p.Clone()
other.ChainHash[0] ^= 1
short := provider.NewArchive(bytes.NewReader(b[:len(b)-1]), int64(len(b)-1))
notArchive := mustRead(t, "../testdata/releases/1000.dkr")
for name, c := range map[string]struct {
a *provider.Archive
p *profile.Profile
}{
"another chain": {a, other},
"a byte missing": {short, p},
"a .dkr": {provider.NewArchive(bytes.NewReader(notArchive), int64(len(notArchive))), p},
"no byte": {provider.NewArchive(bytes.NewReader(nil), 0), p},
} {
if _, err := c.a.Supply(c.p, provider.Condition{Round: 1000}); !onlyUnavailable(err) {
t.Fatalf("%s: %v", name, err)
}
}
}
// onlyUnavailable reports whether err wraps ErrReleaseUnavailable and no
// other normative code.
func onlyUnavailable(err error) bool {
for _, e := range datekeys.All() {
if errors.Is(err, e) != (e == datekeys.ErrReleaseUnavailable) {
return false
}
}
return true
}
func mustRead(t *testing.T, path string) []byte {
t.Helper()
b, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
return b
}
// EncodeRelease never writes an object that DecodeRelease rejects, and a
// release of the profile carries its chain hash.
func TestEncodeRelease(t *testing.T) {
p := profile.Quicknet()
r := testkit.Release(1000)
for _, bad := range []provider.Release{
r,
{Round: 0, Signature: r.Signature, ChainHash: p.ChainHash[:]},
{Round: 1 << 53, Signature: r.Signature, ChainHash: p.ChainHash[:]},
{Round: 1000, ChainHash: p.ChainHash[:]},
{Round: 1000, Signature: make([]byte, 97), ChainHash: p.ChainHash[:]},
} {
if _, err := provider.EncodeRelease(bad); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
t.Fatalf("%+v: %v", bad, err)
}
}
b, err := provider.NewReleaseObject(p, r)
if err != nil || len(b) != 111 {
t.Fatalf("%d bytes, %v", len(b), err)
}
got, err := provider.DecodeRelease(b)
if err != nil || got.Round != 1000 || !bytes.Equal(got.Signature, r.Signature) || !bytes.Equal(got.ChainHash, p.ChainHash[:]) {
t.Fatalf("%+v, %v", got, err)
}
if _, err := provider.EncodeArchiveHeader(p.ChainHash[:], 0, 1); err == nil {
t.Fatal("archive from round 0")
}
}
// FuzzDecodeRelease: whatever the input, DecodeRelease returns exactly one
// normative code, and what it accepts encodes again to the same bytes.
func FuzzDecodeRelease(f *testing.F) {
f.Add(mustReadF(f, "../testdata/releases/1000.dkr"))
f.Add([]byte(`{"round":1000,"signature":"00"}`))
f.Fuzz(func(t *testing.T, b []byte) {
r, err := provider.ParseRelease(b)
if err != nil {
n := 0
for _, e := range datekeys.All() {
if errors.Is(err, e) {
n++
}
}
if n != 1 {
t.Fatalf("%d codes: %v", n, err)
}
return
}
if r.ChainHash == nil {
return
}
if again, err := provider.EncodeRelease(r); err != nil || !bytes.Equal(again, b) {
t.Fatalf("encoding again: %x, %v", again, err)
}
})
}
func mustReadF(f *testing.F, path string) []byte {
b, err := os.ReadFile(path)
if err != nil {
f.Fatal(err)
}
return b
}

Powered by TurnKey Linux.