package testkit import ( "encoding/base64" "encoding/hex" "fmt" "strings" "time" datekeys "g.activething.com/go/DateKeys" "g.activething.com/go/DateKeys/datekey" "g.activething.com/go/DateKeys/profile" ) // SpecVersion is the specification the vectors and fixtures implement. const SpecVersion = "0.8.1" // RoundVector is one Quicknet resolution vector (spec §65). type RoundVector struct { Name string `json:"name"` Requested string `json:"requested"` Round uint64 `json:"round,omitempty"` Effective string `json:"effective,omitempty"` Error string `json:"error,omitempty"` } // RoundVectorFile is testdata/vectors/quicknet_rounds.json. type RoundVectorFile struct { Spec string `json:"spec"` Profile string `json:"profile"` Description string `json:"description"` Vectors []RoundVector `json:"vectors"` } // DK1Vector is one dk1_ vector (spec §66). Valid vectors carry the logical // object and every intermediate encoding; invalid ones carry the input and the // expected error. type DK1Vector struct { Name string `json:"name"` Network string `json:"network,omitempty"` Round uint64 `json:"round,omitempty"` CanonicalJSON string `json:"canonical_json,omitempty"` Base64URL string `json:"base64url,omitempty"` Input string `json:"input,omitempty"` DK1 string `json:"dk1,omitempty"` Error string `json:"error,omitempty"` } // DK1VectorFile is testdata/vectors/dk1.json. type DK1VectorFile struct { Spec string `json:"spec"` Description string `json:"description"` Vectors []DK1Vector `json:"vectors"` } // ProfileVector is testdata/vectors/profile_quicknet.json. type ProfileVector struct { Spec string `json:"spec"` Description string `json:"description"` ProfileID string `json:"profile_id"` Provider string `json:"provider"` Network string `json:"network"` ChainHash string `json:"chain_hash"` PublicKey string `json:"public_key"` PeriodSeconds uint64 `json:"period_seconds"` GenesisTime int64 `json:"genesis_time"` GenesisSeed string `json:"genesis_seed"` Scheme string `json:"scheme"` CanonicalCBOR string `json:"canonical_cbor"` ProfileHash string `json:"profile_hash"` } // RoundVectors computes the Quicknet resolution vectors with the implementation. func RoundVectors() RoundVectorFile { p := profile.Quicknet() g := time.Unix(p.GenesisTime, 0).UTC() r1000, _ := datekey.RoundTime(p, 1000) cases := []struct { name string at time.Time }{ {"genesis exactly: round 1", g}, {"genesis + 1ns: next round", g.Add(time.Nanosecond)}, {"genesis + 1s", g.Add(time.Second)}, {"genesis + one period: round 2", g.Add(3 * time.Second)}, {"genesis + one period + 1ns: round 3", g.Add(3*time.Second + time.Nanosecond)}, {"genesis - 1s: before the profile", g.Add(-time.Second)}, {"round 1000 boundary exactly", r1000}, {"one second before the round 1000 boundary", r1000.Add(-time.Second)}, {"one second after the round 1000 boundary", r1000.Add(time.Second)}, {"1ns after the round 1000 boundary", r1000.Add(time.Nanosecond)}, {"half a second after the round 1000 boundary", r1000.Add(500 * time.Millisecond)}, {"normative vector 2030-01-01 (spec §16)", time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC)}, {"1ns after 2030-01-01", time.Date(2030, 1, 1, 0, 0, 0, 1, time.UTC)}, {"normative vector round 66432123 (spec §16)", time.Date(2029, 12, 16, 7, 15, 33, 0, time.UTC)}, {"offset timezone equals UTC instant", time.Date(2026, 10, 22, 19, 0, 0, 1_000_000, time.FixedZone("", 2*3600))}, {"last representable round time", time.Date(9999, 12, 31, 23, 59, 57, 0, time.UTC)}, {"after the last representable round", time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)}, } f := RoundVectorFile{ Spec: SpecVersion, Profile: p.ID, Description: "Quicknet date to round resolution (spec §15, §16, §65), generated by the reference implementation.", } for _, c := range cases { v := RoundVector{Name: c.name, Requested: c.at.Format(time.RFC3339Nano)} d, err := datekey.Resolve(p, c.at) if err != nil { v.Error = datekeys.Code(err) } else { v.Round = d.Round v.Effective = d.UnlockAt(p).Format(time.RFC3339Nano) } f.Vectors = append(f.Vectors, v) } return f } // DK1Vectors computes the dk1_ vectors with the implementation. func DK1Vectors() DK1VectorFile { f := DK1VectorFile{ Spec: SpecVersion, Description: "Canonical dk1_ strings and rejected encodings (spec §18, §19, §66), generated by the reference implementation.", } for _, v := range []struct { name string round uint64 }{ {"round 1", 1}, {"round 1000", 1000}, {"normative 2030-01-01 round", 66884212}, {"last Quicknet round", profile.Quicknet().MaxRound()}, } { d := datekey.DateKey{ProfileID: profile.QuicknetID, Round: v.round} j := d.CanonicalJSON() f.Vectors = append(f.Vectors, DK1Vector{ Name: v.name, Network: d.ProfileID, Round: d.Round, CanonicalJSON: string(j), Base64URL: base64.RawURLEncoding.EncodeToString(j), DK1: d.Compact(), }) } enc := func(s string) string { return datekey.Prefix + base64.RawURLEncoding.EncodeToString([]byte(s)) } canon := datekey.DateKey{ProfileID: profile.QuicknetID, Round: 66884212} // The canonical JSON of round 1000 is 59 bytes: its Base64 form needs padding // and has unused bits, unlike the 63-byte JSON of round 66884212. r1000 := datekey.DateKey{ProfileID: profile.QuicknetID, Round: 1000} bad := []struct{ name, input string }{ {"whitespace in JSON", enc(`{"version": 1, "network": "datekeys:quicknet:v1", "round": 66884212}`)}, {"keys reordered", enc(`{"network":"datekeys:quicknet:v1","version":1,"round":66884212}`)}, {"trailing whitespace", enc(`{"version":1,"network":"datekeys:quicknet:v1","round":66884212}` + "\n")}, {"exponent notation", enc(`{"version":1,"network":"datekeys:quicknet:v1","round":6.6884212e7}`)}, {"fraction notation", enc(`{"version":1.0,"network":"datekeys:quicknet:v1","round":66884212}`)}, {"escaped character", enc(strings.Replace(string(canon.CanonicalJSON()), "datekeys:", "datekeys\\"+"u003a", 1))}, {"duplicate key", enc(`{"version":1,"network":"datekeys:quicknet:v1","round":1,"round":66884212}`)}, {"padded Base64URL", datekey.Prefix + base64.URLEncoding.EncodeToString(r1000.CanonicalJSON())}, {"non-zero trailing bits", mangleLastChar(r1000.Compact())}, {"extra field", enc(`{"version":1,"network":"datekeys:quicknet:v1","round":66884212,"public_key":"00"}`)}, {"missing field", enc(`{"version":1,"network":"datekeys:quicknet:v1"}`)}, {"version 2", enc(`{"version":2,"network":"datekeys:quicknet:v1","round":66884212}`)}, {"round 0", enc(`{"version":1,"network":"datekeys:quicknet:v1","round":0}`)}, {"negative round", enc(`{"version":1,"network":"datekeys:quicknet:v1","round":-1}`)}, {"fractional round", enc(`{"version":1,"network":"datekeys:quicknet:v1","round":1.5}`)}, {"round above 2^53-1", enc(fmt.Sprintf(`{"version":1,"network":"datekeys:quicknet:v1","round":%d}`, uint64(datekey.MaxRound)+1))}, {"round as string", enc(`{"version":1,"network":"datekeys:quicknet:v1","round":"66884212"}`)}, {"uppercase network", enc(`{"version":1,"network":"DATEKEYS:QUICKNET:V1","round":66884212}`)}, {"trailing data", enc(`{"version":1,"network":"datekeys:quicknet:v1","round":66884212}x`)}, {"byte order mark", enc("\ufeff" + `{"version":1,"network":"datekeys:quicknet:v1","round":66884212}`)}, {"not Base64", datekey.Prefix + "!!!"}, {"missing prefix", canon.Compact()[len(datekey.Prefix):]}, {"uppercase prefix", "DK1_" + canon.Compact()[len(datekey.Prefix):]}, } for _, b := range bad { _, err := datekey.Parse(b.input) code := datekeys.Code(err) if err == nil { code = "accepted" } f.Vectors = append(f.Vectors, DK1Vector{Name: b.name, Input: b.input, Error: code}) } return f } // mangleLastChar changes the last Base64 character to one that decodes to the // same bytes but has non-zero unused bits. func mangleLastChar(s string) string { const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" last := s[len(s)-1] for i := 0; i < len(alphabet); i++ { c := alphabet[i] if c == last { continue } cand := s[:len(s)-1] + string(c) a, errA := base64.RawURLEncoding.DecodeString(s[len(datekey.Prefix):]) b, errB := base64.RawURLEncoding.DecodeString(cand[len(datekey.Prefix):]) if errA == nil && errB == nil && string(a) == string(b) { return cand } } return s } // QuicknetProfileVector computes the profile vector with the implementation. func QuicknetProfileVector() (ProfileVector, error) { p := profile.Quicknet() b, err := p.CanonicalCBOR() if err != nil { return ProfileVector{}, err } h, err := p.Hash() if err != nil { return ProfileVector{}, err } return ProfileVector{ Spec: SpecVersion, Description: "Quicknet Provider Profile V1: exact Deterministic CBOR and profile_hash (spec §11, §12, §75 item 2), generated by the reference implementation.", ProfileID: p.ID, Provider: p.Provider, Network: p.Network, ChainHash: p.ChainHashHex(), PublicKey: hex.EncodeToString(p.PublicKey), PeriodSeconds: uint64(p.Period / time.Second), GenesisTime: p.GenesisTime, GenesisSeed: hex.EncodeToString(p.GenesisSeed[:]), Scheme: p.Scheme, CanonicalCBOR: hex.EncodeToString(b), ProfileHash: hex.EncodeToString(h[:]), }, nil }