Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

registry_test.gno

75.25 Kb · 1566 lines
   1package zones
   2
   3import (
   4	"strconv"
   5	"strings"
   6	"testing"
   7
   8	"gno.land/p/nt/testutils/v0"
   9	"gno.land/p/nt/uassert/v0"
  10)
  11
  12var (
  13	alice   = testutils.TestAddress("alice")
  14	bob     = testutils.TestAddress("bob")
  15	curator = testutils.TestAddress("curator")
  16	carol   = testutils.TestAddress("carol")
  17)
  18
  19func onyx() Info {
  20	return Info{
  21		ChainID:     "onyx-1",
  22		Title:       "Onyx",
  23		Description: "The gno.land testnet.",
  24		Kind:        Testnet,
  25		GnowebURL:   "https://onyx.testnets.gno.land",
  26		RPCURL:      "https://rpc.onyx.testnets.gno.land",
  27	}
  28}
  29
  30func TestProposeAndRead(t *testing.T) {
  31	r := NewRegistry()
  32	in := onyx()
  33	in.Title = "  Onyx  "
  34	uassert.NoError(t, r.Propose(alice, 10, "onyx", in))
  35
  36	z, ok := r.Zone("onyx")
  37	uassert.True(t, ok)
  38	uassert.Equal(t, "Onyx", z.Title) // trimmed
  39	uassert.Equal(t, string(Pending), string(z.Status))
  40	uassert.Equal(t, alice.String(), z.Proposer.String())
  41	uassert.Equal(t, int64(10), z.ProposedAt)
  42	uassert.True(t, !z.Reviewed())
  43
  44	_, ok = r.Zone("nope")
  45	uassert.False(t, ok)
  46
  47	uassert.ErrorContains(t, r.Propose(bob, 11, "onyx", onyx()), "is taken")
  48	uassert.ErrorContains(t, r.Propose(bob, 11, "On yx", onyx()), "slug")
  49	uassert.ErrorContains(t, r.Propose("", 11, "other", onyx()), "proposer is not a valid lowercase address")
  50	uassert.Equal(t, 1, r.Len())
  51}
  52
  53func TestZoneCopiesAreCopies(t *testing.T) {
  54	r := NewRegistry()
  55	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
  56	z, _ := r.Zone("onyx")
  57	z.Title = "mutated"
  58	z.Status = Approved
  59	again, _ := r.Zone("onyx")
  60	uassert.Equal(t, "Onyx", again.Title)
  61	uassert.Equal(t, string(Pending), string(again.Status))
  62}
  63
  64func TestPendingCapPerProposer(t *testing.T) {
  65	r := NewRegistry()
  66	for i := 0; i < MaxPendingPerProposer; i++ {
  67		uassert.NoError(t, r.Propose(alice, 1, "z"+strconv.Itoa(i), onyx()))
  68	}
  69	uassert.ErrorContains(t, r.Propose(alice, 1, "one-more", onyx()), "pending proposals, the limit is")
  70	// Somebody else is not blocked by alice's spam.
  71	uassert.NoError(t, r.Propose(bob, 1, "bobs", onyx()))
  72	// A reviewed proposal frees the slot.
  73	uassert.NoError(t, r.ReviewZone("z0", Approved, rev(r, "z0"), curator, 2, ""))
  74	uassert.NoError(t, r.Propose(alice, 3, "one-more", onyx()))
  75	// So does a removed one.
  76	uassert.NoError(t, r.RemoveZone("z1", rev(r, "z1")))
  77	uassert.NoError(t, r.Propose(alice, 3, "and-another", onyx()))
  78}
  79
  80func TestReviewTransitions(t *testing.T) {
  81	cases := []struct {
  82		from   Status
  83		to     Status
  84		reason string
  85		err    string
  86	}{
  87		{Pending, Approved, "", ""},
  88		{Pending, Rejected, "duplicate of onyx", ""},
  89		{Pending, Rejected, "", "needs a reason"},
  90		{Pending, Rejected, "\u200b", "invisible or bidi"},
  91		{Approved, Retired, "\u2066\u2069", "invisible or bidi"},
  92		{Pending, Retired, "gone", "only an approved zone can be retired"},
  93		{Pending, Pending, "", "already pending"},
  94		{Approved, Retired, "the RPC stopped resolving", ""},
  95		{Approved, Retired, "  ", "needs a reason"},
  96		{Approved, Rejected, "x", "retire it instead"},
  97		{Approved, Approved, "", "already approved"},
  98		{Rejected, Approved, "reconsidered", ""},
  99		{Rejected, Retired, "x", "only an approved zone"},
 100		{Retired, Approved, "it came back", ""},
 101		{Retired, Rejected, "x", "only a pending zone can be rejected"},
 102		{Pending, "official", "", "cannot set a zone"},
 103	}
 104	for _, c := range cases {
 105		r := NewRegistry()
 106		uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
 107		// Walk the zone to the starting state.
 108		switch c.from {
 109		case Approved:
 110			uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 2, ""))
 111		case Rejected:
 112			uassert.NoError(t, r.ReviewZone("onyx", Rejected, rev(r, "onyx"), curator, 2, "setup"))
 113		case Retired:
 114			uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 2, ""))
 115			uassert.NoError(t, r.ReviewZone("onyx", Retired, rev(r, "onyx"), curator, 3, "setup"))
 116		}
 117		label := string(c.from) + " -> " + string(c.to)
 118		err := r.ReviewZone("onyx", c.to, rev(r, "onyx"), curator, 9, c.reason)
 119		z, _ := r.Zone("onyx")
 120		if c.err != "" {
 121			uassert.ErrorContains(t, err, c.err, label)
 122			uassert.Equal(t, string(c.from), string(z.Status), label)
 123			continue
 124		}
 125		uassert.NoError(t, err, label)
 126		uassert.Equal(t, string(c.to), string(z.Status), label)
 127		uassert.Equal(t, curator.String(), z.ReviewedBy.String(), label)
 128		uassert.Equal(t, int64(9), z.ReviewedAt, label)
 129		uassert.Equal(t, c.reason, z.Reason, label)
 130	}
 131}
 132
 133func TestEdit(t *testing.T) {
 134	r := NewRegistry()
 135	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
 136	in := onyx()
 137	in.GenesisURL = "https://github.com/gnolang/gno/releases/download/chain/onyx/genesis.json"
 138	uassert.ErrorContains(t, r.Edit("onyx", rev(r, "onyx"), in, "", 2, ""), "editor is not a valid lowercase address")
 139	uassert.NoError(t, r.Edit("onyx", rev(r, "onyx"), in, alice, 2, ""))
 140	z, _ := r.Zone("onyx")
 141	uassert.Equal(t, in.GenesisURL, z.GenesisURL)
 142	uassert.False(t, z.Reviewed()) // pending: just an edit
 143
 144	bad := in
 145	bad.RPCURL = ""
 146	uassert.ErrorContains(t, r.Edit("onyx", rev(r, "onyx"), bad, alice, 2, ""), "rpc url is empty")
 147	uassert.ErrorContains(t, r.Edit("nope", rev(r, "nope"), in, alice, 2, ""), "no zone")
 148	z, _ = r.Zone("onyx")
 149	uassert.Equal(t, "https://rpc.onyx.testnets.gno.land", z.RPCURL) // unchanged by the refused edit
 150
 151	// Once reviewed, an edit is a decision: it needs a reason and an editor, and
 152	// it replaces the review on record.
 153	uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 3, "probed"))
 154	in.RPCURL = "https://rpc2.onyx.example.com"
 155	uassert.ErrorContains(t, r.Edit("onyx", rev(r, "onyx"), in, curator, 4, ""), "an edit needs a reason")
 156	uassert.ErrorContains(t, r.Edit("onyx", rev(r, "onyx"), in, "", 4, "moved"), "editor is not a valid lowercase address")
 157	z, _ = r.Zone("onyx")
 158	uassert.Equal(t, "https://rpc.onyx.testnets.gno.land", z.RPCURL)
 159	uassert.Equal(t, "probed", z.Reason)
 160	uassert.NoError(t, r.Edit("onyx", rev(r, "onyx"), in, bob, 5, "rpc moved"))
 161	z, _ = r.Zone("onyx")
 162	uassert.Equal(t, "https://rpc2.onyx.example.com", z.RPCURL)
 163	uassert.Equal(t, bob.String(), z.ReviewedBy.String())
 164	uassert.Equal(t, int64(5), z.ReviewedAt)
 165	uassert.Equal(t, "rpc moved", z.Reason)
 166	uassert.Equal(t, string(Approved), string(z.Status))
 167
 168	// A retired zone is a record: its retirement reason is not editable away.
 169	uassert.NoError(t, r.ReviewZone("onyx", Retired, rev(r, "onyx"), curator, 6, "testnet ended"))
 170	uassert.ErrorContains(t, r.Edit("onyx", rev(r, "onyx"), in, curator, 7, "typo"), "only a pending or approved zone can be edited")
 171	z, _ = r.Zone("onyx")
 172	uassert.Equal(t, "testnet ended", z.Reason)
 173	// Nor is a rejected one.
 174	uassert.NoError(t, r.Propose(alice, 8, "nope", onyx()))
 175	uassert.NoError(t, r.ReviewZone("nope", Rejected, rev(r, "nope"), curator, 9, "dup"))
 176	uassert.ErrorContains(t, r.Edit("nope", rev(r, "nope"), onyx(), alice, 10, ""), "only a pending or approved zone can be edited")
 177}
 178
 179// What was verified is that an endpoint answered for the zone's chain id, so
 180// changing the chain id un-verifies them; changing only the RPC does not.
 181func TestChainIDEditUnverifiesEndpoints(t *testing.T) {
 182	r := NewRegistry()
 183	uassert.NoError(t, r.Propose(curator, 1, "onyx", onyx()))
 184	uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 1, ""))
 185	a, _ := r.Register(alice, 2, "onyx", RPC, "https://a.example.com", "")
 186	b, _ := r.Register(alice, 2, "onyx", Peer, nodeID+"@b.example.com:26656", "")
 187	f, _ := r.Register(alice, 2, "onyx", RPC, "https://f.example.com", "")
 188	uassert.NoError(t, r.ReviewEndpoint(a, Verified, zr(r, a), erev(r, a), curator, 3, ""))
 189	uassert.NoError(t, r.ReviewEndpoint(b, Verified, zr(r, b), erev(r, b), curator, 3, ""))
 190	uassert.NoError(t, r.ReviewEndpoint(f, Flagged, zr(r, f), erev(r, f), curator, 3, "down"))
 191
 192	in := onyx()
 193	in.RPCURL = "https://rpc2.onyx.example.com"
 194	uassert.NoError(t, r.Edit("onyx", rev(r, "onyx"), in, curator, 4, "rpc moved"))
 195	uassert.Equal(t, 2, len(r.Endpoints(EndpointFilter{Zone: "onyx", Status: Verified})))
 196
 197	in.ChainID = "onyx-2"
 198	uassert.NoError(t, r.Edit("onyx", rev(r, "onyx"), in, curator, 5, "relaunched as onyx-2"))
 199	uassert.Equal(t, 0, len(r.Endpoints(EndpointFilter{Zone: "onyx", Status: Verified})))
 200	e, _ := r.Endpoint(a)
 201	uassert.Equal(t, string(Unverified), string(e.Status))
 202	uassert.Equal(t, ChainIDChanged, e.Reason)
 203	uassert.Equal(t, int64(5), e.ReviewedAt)
 204	e, _ = r.Endpoint(f)
 205	uassert.Equal(t, string(Flagged), string(e.Status)) // a flag is not undone by an edit
 206	uassert.Equal(t, "down", e.Reason)
 207	// And the reset ones count against the review queue again (the flagged
 208	// one has its verdict and stays out): verifying one frees it.
 209	v, _ := r.Count("onyx")
 210	uassert.Equal(t, 0, v)
 211	uassert.Equal(t, 2, r.Awaiting("onyx"), "both reset endpoints are back in the queue")
 212	uassert.NoError(t, r.ReviewEndpoint(a, Verified, zr(r, a), erev(r, a), curator, 6, "answers onyx-2"))
 213	uassert.Equal(t, 1, len(r.Endpoints(EndpointFilter{Zone: "onyx", Status: Verified})))
 214	v, _ = r.Count("onyx")
 215	uassert.Equal(t, 1, v)
 216
 217	// Retiring resets what was verified, at the moment the network stops: a
 218	// retired network's hosts may be somebody else's by the time anyone reads
 219	// them. Approving it again does not restore anything.
 220	uassert.NoError(t, r.ReviewEndpoint(b, Verified, zr(r, b), erev(r, b), curator, 6, ""))
 221	uassert.NoError(t, r.ReviewZone("onyx", Retired, rev(r, "onyx"), curator, 6, "stopped"))
 222	uassert.Equal(t, 0, len(r.Endpoints(EndpointFilter{Zone: "onyx", Status: Verified})))
 223	e, _ = r.Endpoint(b)
 224	uassert.Equal(t, ZoneRetired, e.Reason)
 225	uassert.Equal(t, curator.String(), e.ReviewedBy.String())
 226	uassert.ErrorContains(t, r.ReviewEndpoint(b, Verified, zr(r, b), erev(r, b), curator, 6, ""), "nothing on it can be verified")
 227	uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 7, "back up"))
 228	uassert.Equal(t, 0, len(r.Endpoints(EndpointFilter{Zone: "onyx", Status: Verified})))
 229	e, _ = r.Endpoint(f)
 230	uassert.Equal(t, string(Flagged), string(e.Status))
 231	// A pending zone is no exception: a proposer collecting verdicts for one
 232	// chain id and then switching it gets them all reset.
 233	uassert.NoError(t, r.Propose(alice, 7, "pend", onyx()))
 234	pid, _ := r.Register(alice, 7, "pend", RPC, "https://p.example.com", "")
 235	uassert.NoError(t, r.ReviewEndpoint(pid, Verified, zr(r, pid), erev(r, pid), curator, 8, ""))
 236	other := onyx()
 237	other.ChainID = "elsewhere-1"
 238	uassert.NoError(t, r.Edit("pend", rev(r, "pend"), other, alice, 9, ""))
 239	e, _ = r.Endpoint(pid)
 240	uassert.Equal(t, string(Unverified), string(e.Status))
 241	// The proposer's own edit caused the reset, and the proposer is not a
 242	// reviewer, so none is recorded, and the reason says why.
 243	uassert.Equal(t, "", e.ReviewedBy.String())
 244	uassert.Equal(t, ChainIDChanged, e.Reason)
 245	z, _ := r.Zone("pend")
 246	uassert.False(t, z.Reviewed()) // still a pending edit: the zone's own review is untouched
 247}
 248
 249func TestRegisterAndFilter(t *testing.T) {
 250	r := NewRegistry()
 251	uassert.NoError(t, r.Propose(curator, 1, "onyx", onyx()))
 252	uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 1, ""))
 253
 254	rpc, err := r.Register(alice, 5, "onyx", RPC, " https://rpc.onyx.testnets.gno.land ", "gno core")
 255	uassert.NoError(t, err)
 256	peer, err := r.Register(bob, 6, "onyx", Peer, nodeID+"@seed-1.onyx.testnets.gno.land:26656", "")
 257	uassert.NoError(t, err)
 258	uassert.True(t, rpc > 0 && peer > rpc)
 259
 260	e, ok := r.Endpoint(rpc)
 261	uassert.True(t, ok)
 262	uassert.Equal(t, rpc, e.ID)
 263	uassert.Equal(t, "https://rpc.onyx.testnets.gno.land", e.Address) // trimmed
 264	uassert.Equal(t, string(Unverified), string(e.Status))
 265	uassert.Equal(t, alice.String(), e.Registrant.String())
 266
 267	// Dedup is case-insensitive: the same host under another spelling.
 268	_, err = r.Register(bob, 7, "onyx", RPC, "https://RPC.onyx.testnets.gno.land", "")
 269	uassert.ErrorContains(t, err, "already lists that rpc")
 270	// ...but a path is case-sensitive, so another path is another endpoint.
 271	_, err = r.Register(bob, 7, "onyx", Indexer, "https://indexer.example.com/API", "")
 272	uassert.NoError(t, err)
 273	_, err = r.Register(bob, 7, "onyx", Indexer, "https://INDEXER.example.com/API", "")
 274	uassert.ErrorContains(t, err, "already lists that indexer")
 275	_, err = r.Register(bob, 7, "onyx", Indexer, "https://indexer.example.com/api", "")
 276	uassert.NoError(t, err)
 277	// The same address under another kind is a different endpoint.
 278	_, err = r.Register(bob, 7, "onyx", Gnoweb, "https://rpc.onyx.testnets.gno.land", "")
 279	uassert.NoError(t, err)
 280
 281	_, err = r.Register(bob, 7, "nope", RPC, "https://x.y", "")
 282	uassert.ErrorContains(t, err, "no zone")
 283	_, err = r.Register(bob, 7, "onyx", Peer, "https://x.y", "")
 284	uassert.ErrorContains(t, err, "<node id>@")
 285	_, err = r.Register(bob, 7, "onyx", RPC, "https://x.y", "two\nlines")
 286	uassert.ErrorContains(t, err, "control character")
 287
 288	uassert.Equal(t, 5, len(r.Endpoints(EndpointFilter{Zone: "onyx"})))
 289	uassert.Equal(t, 1, len(r.Endpoints(EndpointFilter{Zone: "onyx", Kind: Peer})))
 290	uassert.Equal(t, 4, len(r.Endpoints(EndpointFilter{Registrant: bob})))
 291	uassert.Equal(t, 0, len(r.Endpoints(EndpointFilter{Zone: "onyx", Status: Verified})))
 292
 293	uassert.NoError(t, r.ReviewEndpoint(rpc, Verified, zr(r, rpc), erev(r, rpc), curator, 8, "answers onyx-1"))
 294	vs := r.Endpoints(EndpointFilter{Zone: "onyx", Status: Verified})
 295	uassert.Equal(t, 1, len(vs))
 296	uassert.Equal(t, "answers onyx-1", vs[0].Reason)
 297	v, total := r.Count("onyx")
 298	uassert.Equal(t, 1, v)
 299	uassert.Equal(t, 5, total)
 300
 301	uassert.ErrorContains(t, r.ReviewEndpoint(rpc, Verified, zr(r, rpc), erev(r, rpc), curator, 8, "answers onyx-1"), "already verified")
 302	uassert.ErrorContains(t, r.ReviewEndpoint(peer, Flagged, zr(r, peer), erev(r, peer), curator, 8, ""), "needs a reason")
 303	uassert.ErrorContains(t, r.ReviewEndpoint(peer, "trusted", zr(r, peer), erev(r, peer), curator, 8, ""), "unknown verification")
 304	uassert.ErrorContains(t, r.ReviewEndpoint(peer, "", zr(r, peer), erev(r, peer), curator, 8, ""), "needs a verdict")
 305	uassert.ErrorContains(t, r.ReviewEndpoint(peer, " verified ", zr(r, peer), erev(r, peer), curator, 8, ""), "unknown verification")
 306	_, err = r.Register(bob, 7, "onyx", " rpc ", "not a url", "")
 307	uassert.ErrorContains(t, err, "unknown endpoint kind")
 308	// A scheme's case is not a different endpoint.
 309	_, err = r.Register(bob, 7, "onyx", Explorer, "HTTPS://Explorer.example.com/x", "")
 310	uassert.NoError(t, err)
 311	_, err = r.Register(bob, 7, "onyx", Explorer, "https://explorer.example.com/x", "")
 312	uassert.ErrorContains(t, err, "already lists that explorer")
 313	uassert.ErrorContains(t, r.ReviewEndpoint(999, Verified, zr(r, 999), erev(r, 999), curator, 8, ""), "no endpoint #999")
 314	uassert.ErrorContains(t, r.ReviewEndpoint(0, Verified, zr(r, 0), erev(r, 0), curator, 8, ""), "no endpoint #0")
 315	uassert.NoError(t, r.ReviewEndpoint(peer, Flagged, zr(r, peer), erev(r, peer), curator, 9, "wrong chain id"))
 316	uassert.NoError(t, r.ReviewEndpoint(peer, Unverified, zr(r, peer), erev(r, peer), curator, 10, "fixed, re-checking"))
 317
 318	// Removing frees the dedup key, so the same address can come back.
 319	uassert.NoError(t, r.RemoveEndpoint(rpc, erev(r, rpc)))
 320	_, ok = r.Endpoint(rpc)
 321	uassert.False(t, ok)
 322	uassert.ErrorContains(t, r.RemoveEndpoint(rpc, erev(r, rpc)), "no endpoint")
 323	_, err = r.Register(bob, 11, "onyx", RPC, "https://rpc.onyx.testnets.gno.land", "")
 324	uassert.NoError(t, err)
 325}
 326
 327func TestRegisterNeedsALiveZone(t *testing.T) {
 328	r := NewRegistry()
 329	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
 330	// Pending takes endpoints: a proposer fills in the peers before review.
 331	_, err := r.Register(alice, 2, "onyx", RPC, "https://a.example.com", "")
 332	uassert.NoError(t, err)
 333	uassert.NoError(t, r.ReviewZone("onyx", Rejected, rev(r, "onyx"), curator, 3, "not a gno chain"))
 334	_, err = r.Register(alice, 4, "onyx", RPC, "https://b.example.com", "")
 335	uassert.ErrorContains(t, err, "is rejected and takes no endpoints")
 336}
 337
 338func TestEndpointCapPerAddress(t *testing.T) {
 339	r := NewRegistry()
 340	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
 341	for i := 0; i < MaxEndpointsPerAddress; i++ {
 342		_, err := r.Register(alice, 2, "onyx", RPC, "https://n"+strconv.Itoa(i)+".example.com", "")
 343		uassert.NoError(t, err)
 344	}
 345	_, err := r.Register(alice, 2, "onyx", RPC, "https://over.example.com", "")
 346	uassert.ErrorContains(t, err, "the limit is")
 347	// Bob still can.
 348	_, err = r.Register(bob, 2, "onyx", RPC, "https://over.example.com", "")
 349	uassert.NoError(t, err)
 350}
 351
 352func TestRemoveZone(t *testing.T) {
 353	r := NewRegistry()
 354	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
 355	_, err := r.Register(bob, 2, "onyx", RPC, "https://a.example.com", "")
 356	uassert.NoError(t, err)
 357	uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 3, ""))
 358	uassert.ErrorContains(t, r.RemoveZone("onyx", rev(r, "onyx")), "retire it instead")
 359	uassert.NoError(t, r.ReviewZone("onyx", Retired, rev(r, "onyx"), curator, 4, "testnet ended"))
 360	uassert.ErrorContains(t, r.RemoveZone("onyx", rev(r, "onyx")), "kept on record")
 361	_, ok := r.Zone("onyx")
 362	uassert.True(t, ok)
 363
 364	// A rejected zone is removable, endpoints and all.
 365	uassert.NoError(t, r.Propose(alice, 5, "spam", onyx()))
 366	_, err = r.Register(bob, 5, "spam", RPC, "https://a.example.com", "")
 367	uassert.NoError(t, err)
 368	uassert.NoError(t, r.ReviewZone("spam", Rejected, rev(r, "spam"), curator, 6, "not a network"))
 369	uassert.NoError(t, r.RemoveZone("spam", rev(r, "spam")))
 370	_, ok = r.Zone("spam")
 371	uassert.False(t, ok)
 372	uassert.Equal(t, 0, len(r.Endpoints(EndpointFilter{Zone: "spam"})))
 373	uassert.Equal(t, 1, r.Len())
 374	uassert.ErrorContains(t, r.RemoveZone("spam", rev(r, "spam")), "no zone")
 375	// The slug and the address are free again.
 376	uassert.NoError(t, r.Propose(bob, 7, "spam", onyx()))
 377	_, err = r.Register(bob, 8, "spam", RPC, "https://a.example.com", "")
 378	uassert.NoError(t, err)
 379}
 380
 381func TestReviewNeedsAReviewer(t *testing.T) {
 382	r := NewRegistry()
 383	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
 384	uassert.ErrorContains(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), "", 2, ""), "reviewer is not a valid lowercase address")
 385	z, _ := r.Zone("onyx")
 386	uassert.Equal(t, string(Pending), string(z.Status))
 387	id, err := r.Register(alice, 2, "onyx", RPC, "https://a.example.com", "")
 388	uassert.NoError(t, err)
 389	uassert.ErrorContains(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), "", 3, ""), "reviewer is not a valid lowercase address")
 390}
 391
 392// A flood from many addresses fills the review queue and nothing above it.
 393func TestFloodCannotCrowdOutReviewedEntries(t *testing.T) {
 394	r := NewRegistry()
 395	uassert.NoError(t, r.Propose(curator, 1, "onyx", onyx()))
 396	uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 1, ""))
 397	for i := 0; i < MaxPending; i++ {
 398		uassert.NoError(t, r.Propose(testAddr(i), 2, "flood-"+strconv.Itoa(i), onyx()))
 399	}
 400	uassert.ErrorContains(t, r.Propose(bob, 3, "honest", onyx()), "waiting for review")
 401	uassert.Equal(t, MaxPending+1, r.Len())
 402	// Clearing one makes room.
 403	uassert.NoError(t, r.ReviewZone("flood-0", Rejected, rev(r, "flood-0"), curator, 4, "spam"))
 404	uassert.NoError(t, r.Propose(bob, 5, "honest", onyx()))
 405
 406	// Endpoints: those waiting for a verdict have their own gate, and verifying frees it.
 407	ids := []int64{}
 408	for i := 0; i < MaxUnverifiedPerZone; i++ {
 409		id, err := r.Register(testAddr(i/MaxEndpointsPerAddress), 6, "onyx", RPC, "https://n"+strconv.Itoa(i)+".example.com", "")
 410		uassert.NoError(t, err)
 411		ids = append(ids, id)
 412	}
 413	_, err := r.Register(bob, 7, "onyx", RPC, "https://honest.example.com", "")
 414	uassert.ErrorContains(t, err, "waiting for review")
 415	uassert.NoError(t, r.ReviewEndpoint(ids[0], Verified, zr(r, ids[0]), erev(r, ids[0]), curator, 8, ""))
 416	_, err = r.Register(bob, 9, "onyx", RPC, "https://honest.example.com", "")
 417	uassert.NoError(t, err)
 418	// The gate is full again (the honest one is waiting). A flag is a verdict,
 419	// so a flagged endpoint leaves the queue: curators keep the warning and
 420	// still make room, instead of deleting the warning to make room.
 421	_, err = r.Register(bob, 11, "onyx", RPC, "https://honest2.example.com", "")
 422	uassert.ErrorContains(t, err, "waiting for review")
 423	uassert.NoError(t, r.ReviewEndpoint(ids[1], Flagged, zr(r, ids[1]), erev(r, ids[1]), curator, 12, "spam"))
 424	_, err = r.Register(bob, 13, "onyx", RPC, "https://honest2.example.com", "")
 425	uassert.NoError(t, err)
 426	// Unflagging puts it back in the queue, and a review is never refused for
 427	// the queue being full, only a registration is.
 428	uassert.NoError(t, r.ReviewEndpoint(ids[1], Unverified, zr(r, ids[1]), erev(r, ids[1]), curator, 14, "re-checking"))
 429	_, err = r.Register(bob, 15, "onyx", RPC, "https://honest3.example.com", "")
 430	uassert.ErrorContains(t, err, "waiting for review")
 431	uassert.NoError(t, r.RemoveEndpoint(ids[2], erev(r, ids[2])))
 432	uassert.NoError(t, r.RemoveEndpoint(ids[3], erev(r, ids[3])))
 433	_, err = r.Register(bob, 16, "onyx", RPC, "https://honest3.example.com", "")
 434	uassert.NoError(t, err)
 435}
 436
 437func testAddr(i int) address { return testutils.TestAddress("flood" + strconv.Itoa(i)) }
 438
 439func TestZonesFilterAndOrder(t *testing.T) {
 440	r := NewRegistry()
 441	mainnet := onyx()
 442	mainnet.ChainID, mainnet.Kind = "gnoland-1", Mainnet
 443	uassert.NoError(t, r.Propose(curator, 1, "mainnet", mainnet))
 444	uassert.NoError(t, r.Propose(curator, 1, "onyx", onyx()))
 445	uassert.NoError(t, r.Propose(alice, 2, "alices", onyx()))
 446	uassert.NoError(t, r.ReviewZone("mainnet", Approved, rev(r, "mainnet"), curator, 3, ""))
 447	uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 3, ""))
 448
 449	slugs := func(zs []Zone) string {
 450		s := ""
 451		for _, z := range zs {
 452			s += z.Slug + " "
 453		}
 454		return s
 455	}
 456	uassert.Equal(t, "mainnet onyx alices ", slugs(r.Zones(ZoneFilter{})))
 457	uassert.Equal(t, "mainnet onyx ", slugs(r.Zones(ZoneFilter{Status: Approved})))
 458	uassert.Equal(t, "alices ", slugs(r.Zones(ZoneFilter{Status: Pending})))
 459	uassert.Equal(t, "onyx alices ", slugs(r.Zones(ZoneFilter{Kind: Testnet})))
 460	uassert.Equal(t, "onyx ", slugs(r.Zones(ZoneFilter{Status: Approved, Kind: Testnet})))
 461	uassert.Equal(t, "", slugs(r.Zones(ZoneFilter{Kind: Local})))
 462}
 463
 464func TestPagesAndCounts(t *testing.T) {
 465	r := NewRegistry()
 466	for i := 0; i < 7; i++ {
 467		uassert.NoError(t, r.Propose(testAddr(i), 1, "z"+strconv.Itoa(i), onyx()))
 468	}
 469	uassert.NoError(t, r.ReviewZone("z1", Approved, rev(r, "z1"), curator, 2, ""))
 470	uassert.NoError(t, r.ReviewZone("z3", Approved, rev(r, "z3"), curator, 2, ""))
 471	uassert.NoError(t, r.ReviewZone("z5", Approved, rev(r, "z5"), curator, 2, ""))
 472	uassert.Equal(t, 3, r.ZoneCount(Approved))
 473	uassert.Equal(t, 4, r.ZoneCount(Pending))
 474	uassert.Equal(t, 7, r.ZoneCount(""))
 475
 476	page := func(zs []Zone) string {
 477		out := ""
 478		for _, z := range zs {
 479			out += z.Slug + " "
 480		}
 481		return out
 482	}
 483	uassert.Equal(t, "z1 z3 ", page(r.ZonePage(Approved, 1, 2)))
 484	uassert.Equal(t, "z5 ", page(r.ZonePage(Approved, 2, 2)))
 485	uassert.Equal(t, "", page(r.ZonePage(Approved, 3, 2)))
 486	uassert.Equal(t, "", page(r.ZonePage(Approved, 0, 2)))
 487	uassert.Equal(t, "", page(r.ZonePage(Approved, 1, 0)))
 488	uassert.Equal(t, "", page(r.ZonePage(Approved, 1<<62, 1<<10)), "no overflow into a small offset")
 489	uassert.Equal(t, "z0 z2 z4 z6 ", page(r.ZonePage(Pending, 1, 10)))
 490	// "" is every status, consistent with ZoneCount("") and ZoneFilter{}.
 491	uassert.Equal(t, "z0 z1 z2 ", page(r.ZonePage("", 1, 3)))
 492	uassert.Equal(t, "z6 ", page(r.ZonePage("", 3, 3)))
 493	uassert.Equal(t, "", page(r.ZonePage("", 4, 3)))
 494	uassert.Equal(t, "", page(r.ZonePage("", 0, 3)))
 495	uassert.Equal(t, "", page(r.ZonePage("", 1<<62, 1<<10)), "no overflow into a small offset")
 496	uassert.Equal(t, "z0 z1 z2 z3 z4 z5 z6 ", page(r.ZonePage("", 1, 1<<40)))
 497	// The status index follows a review and a removal.
 498	uassert.NoError(t, r.ReviewZone("z0", Rejected, rev(r, "z0"), curator, 3, "dup"))
 499	uassert.NoError(t, r.RemoveZone("z2", rev(r, "z2")))
 500	uassert.Equal(t, "z4 z6 ", page(r.ZonePage(Pending, 1, 10)))
 501	uassert.Equal(t, "z0 ", page(r.ZonePage(Rejected, 1, 10)))
 502	uassert.Equal(t, "z1 z3 z5 ", page(r.Zones(ZoneFilter{Status: Approved})))
 503
 504	// Chain ids are not unique, only approved zones are indexed by one (so a
 505	// proposal naming a real chain id costs a reader nothing), and the index
 506	// follows an edit and a retirement.
 507	uassert.Equal(t, "z1 z3 z5 ", page(r.ApprovedByChainID("onyx-1")))
 508	in := onyx()
 509	in.ChainID = "onyx-2"
 510	uassert.NoError(t, r.Edit("z1", rev(r, "z1"), in, curator, 4, "relaunched"))
 511	uassert.Equal(t, "z1 ", page(r.ApprovedByChainID("onyx-2")))
 512	uassert.Equal(t, "z3 z5 ", page(r.ApprovedByChainID("onyx-1")))
 513	uassert.NoError(t, r.Edit("z4", rev(r, "z4"), in, alice, 4, "")) // pending: not indexed
 514	uassert.Equal(t, "z1 ", page(r.ApprovedByChainID("onyx-2")))
 515	uassert.NoError(t, r.ReviewZone("z5", Retired, rev(r, "z5"), curator, 4, "gone"))
 516	uassert.Equal(t, "z3 ", page(r.ApprovedByChainID("onyx-1")))
 517
 518	// Endpoints: counts from the indexes, pages by kind.
 519	for i := 0; i < 5; i++ {
 520		_, err := r.Register(alice, 5, "z3", RPC, "https://e"+strconv.Itoa(i)+".example.com", "")
 521		uassert.NoError(t, err)
 522	}
 523	pid, err := r.Register(alice, 5, "z3", Peer, nodeID+"@p.example.com:26656", "")
 524	uassert.NoError(t, err)
 525	uassert.NoError(t, r.ReviewEndpoint(pid, Verified, zr(r, pid), erev(r, pid), curator, 6, ""))
 526	v, total := r.Count("z3")
 527	uassert.Equal(t, 1, v)
 528	uassert.Equal(t, 6, total)
 529	uassert.Equal(t, 5, r.EndpointCount("z3", RPC))
 530	uassert.Equal(t, 1, r.EndpointCount("z3", Peer))
 531	uassert.Equal(t, 0, r.EndpointCount("z3", Faucet))
 532	uassert.Equal(t, 6, r.EndpointCount("z3", ""))
 533	uassert.Equal(t, 1, len(r.EndpointPage("z3", RPC, 3, 2))) // page 3 of 5 rpcs at 2 a page holds one
 534	uassert.Equal(t, "https://e4.example.com", r.EndpointPage("z3", RPC, 3, 2)[0].Address)
 535	uassert.Equal(t, nodeID+"@p.example.com:26656", r.EndpointPage("z3", Peer, 1, 10)[0].Address)
 536	uassert.Equal(t, 6, len(r.EndpointPage("z3", "", 1, 10)))
 537	uassert.Equal(t, 1, len(r.Endpoints(EndpointFilter{Zone: "z3", Kind: Peer})))
 538	uassert.NoError(t, r.RemoveEndpoint(pid, erev(r, pid)))
 539	uassert.Equal(t, 0, r.EndpointCount("z3", Peer))
 540	v, total = r.Count("z3")
 541	uassert.Equal(t, 0, v)
 542	uassert.Equal(t, 5, total)
 543}
 544
 545// rev is a zone's current revision, what a curator who just read it approves.
 546func rev(r *Registry, slug string) int64 {
 547	z, _ := r.Zone(slug)
 548	return z.Revision
 549}
 550
 551// erev is an endpoint's revision, what a verdict or a removal names.
 552func erev(r *Registry, id int64) int64 {
 553	e, ok := r.Endpoint(id)
 554	if !ok {
 555		return 0
 556	}
 557	return e.Revision
 558}
 559
 560// zr is the revision of an endpoint's zone, what a verification also names.
 561func zr(r *Registry, id int64) int64 {
 562	e, ok := r.Endpoint(id)
 563	if !ok {
 564		return 0
 565	}
 566	return rev(r, e.Zone)
 567}
 568
 569// An approval names the revision the curator read. An edit landing between the
 570// reading and the approval makes the approval fail, rather than making text the
 571// curator never saw official under their name. Remove-and-propose-again cannot
 572// reuse a revision either.
 573func TestApprovalBindsToRevision(t *testing.T) {
 574	r := NewRegistry()
 575	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
 576	read := rev(r, "onyx")
 577
 578	swapped := onyx()
 579	swapped.RPCURL = "https://attacker.example.com"
 580	uassert.NoError(t, r.Edit("onyx", rev(r, "onyx"), swapped, alice, 2, ""))
 581	uassert.ErrorContains(t, r.ReviewZone("onyx", Approved, read, curator, 3, ""), "changed since you read it")
 582	z, _ := r.Zone("onyx")
 583	uassert.Equal(t, string(Pending), string(z.Status))
 584	uassert.Equal(t, alice.String(), z.EditedBy.String())
 585	uassert.Equal(t, int64(2), z.EditedAt)
 586
 587	// Removed and proposed again under the same slug: a new revision, never the old one.
 588	uassert.NoError(t, r.RemoveZone("onyx", rev(r, "onyx")))
 589	uassert.NoError(t, r.Propose(alice, 4, "onyx", onyx()))
 590	uassert.ErrorContains(t, r.ReviewZone("onyx", Approved, read, curator, 5, ""), "changed since you read it")
 591	uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 5, ""))
 592
 593	// Verifying an endpoint binds the same way: to the chain id it was checked against.
 594	id, err := r.Register(alice, 6, "onyx", RPC, "https://a.example.com", "")
 595	uassert.NoError(t, err)
 596	checked := zr(r, id)
 597	moved := onyx()
 598	moved.ChainID = "onyx-2"
 599	uassert.NoError(t, r.Edit("onyx", rev(r, "onyx"), moved, curator, 7, "relaunch"))
 600	uassert.ErrorContains(t, r.ReviewEndpoint(id, Verified, checked, erev(r, id), curator, 8, ""), "changed since you checked it against it")
 601	// A revision from the future is as wrong as a past one.
 602	uassert.ErrorContains(t, r.ReviewEndpoint(id, Verified, zr(r, id)+1, erev(r, id), curator, 8, ""), "changed since")
 603	uassert.ErrorContains(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id)+1, curator, 8, ""), "changed since")
 604	uassert.NoError(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), curator, 8, ""))
 605	// Every verdict binds to the endpoint as read, too: a verify written before
 606	// another curator's flag fails rather than silently reversing it.
 607	read = erev(r, id)
 608	uassert.NoError(t, r.ReviewEndpoint(id, Flagged, zr(r, id), read, curator, 9, "down"))
 609	uassert.ErrorContains(t, r.ReviewEndpoint(id, Unverified, zr(r, id), read, alice, 10, ""), "changed since you checked it")
 610	// The same verdict again, with a new reason, restates it.
 611	uassert.NoError(t, r.ReviewEndpoint(id, Flagged, zr(r, id), erev(r, id), curator, 11, "down since block 9"))
 612	uassert.ErrorContains(t, r.ReviewEndpoint(id, Flagged, zr(r, id), erev(r, id), curator, 12, "down since block 9"), "restating it needs a new reason")
 613}
 614
 615func TestPendingEditTakesNoReason(t *testing.T) {
 616	r := NewRegistry()
 617	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
 618	uassert.ErrorContains(t, r.Edit("onyx", rev(r, "onyx"), onyx(), alice, 2, "fixed a typo"), "takes no reason")
 619	uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 3, ""))
 620	uassert.ErrorContains(t, r.Edit("onyx", rev(r, "onyx"), onyx(), curator, 4, "\u200b"), "invisible")
 621	uassert.ErrorContains(t, r.Edit("onyx", rev(r, "onyx"), onyx(), curator, 4, ""), "needs a reason")
 622	uassert.ErrorContains(t, r.Edit("onyx", rev(r, "onyx"), onyx(), curator, 4, "no change"), "changes nothing")
 623	in := onyx()
 624	in.Title = "Onyx testnet"
 625	uassert.NoError(t, r.Edit("onyx", rev(r, "onyx"), in, curator, 4, "retitled"))
 626}
 627
 628// Rejected and retired zones are kept for the record, each up to a cap of its
 629// own, and neither counts against the live registry: nothing a proposer or a
 630// curator does, and no amount of time, fills it for good.
 631func TestRecordsNeverFillTheRegistry(t *testing.T) {
 632	r := NewRegistry()
 633	for i := 0; i < MaxRejected+3; i++ {
 634		slug := "rej" + strconv.Itoa(i)
 635		uassert.NoError(t, r.Propose(testAddr(i), 1, slug, onyx()))
 636		uassert.NoError(t, r.ReviewZone(slug, Rejected, rev(r, slug), curator, 2, "spam"))
 637	}
 638	uassert.Equal(t, MaxRejected, r.ZoneCount(Rejected))
 639	// The three that entered the rejected state first made room.
 640	_, ok := r.Zone("rej0")
 641	uassert.False(t, ok)
 642	_, ok = r.Zone("rej3")
 643	uassert.True(t, ok)
 644	uassert.Equal(t, 0, r.Live())
 645
 646	for i := 0; i < MaxRetired+2; i++ {
 647		slug := "ret" + strconv.Itoa(i)
 648		uassert.NoError(t, r.Propose(curator, 3, slug, onyx()))
 649		uassert.NoError(t, r.ReviewZone(slug, Approved, rev(r, slug), curator, 3, ""))
 650		uassert.NoError(t, r.ReviewZone(slug, Retired, rev(r, slug), curator, 4, "stopped"))
 651	}
 652	uassert.Equal(t, MaxRetired, r.ZoneCount(Retired))
 653	_, ok = r.Zone("ret0")
 654	uassert.False(t, ok)
 655	uassert.Equal(t, 0, r.Live())
 656	uassert.Equal(t, MaxRejected+MaxRetired, r.Len())
 657
 658	// The live registry is untouched by all of it.
 659	uassert.NoError(t, r.Propose(bob, 5, "honest", onyx()))
 660	uassert.Equal(t, 1, r.Live())
 661}
 662
 663// An evicted zone takes its endpoints and every index entry with it, so its
 664// slug and its addresses can be used again.
 665func TestEvictionUnwindsEverything(t *testing.T) {
 666	r := NewRegistry()
 667	uassert.NoError(t, r.Propose(alice, 1, "first", onyx()))
 668	_, err := r.Register(alice, 1, "first", RPC, "https://f.example.com", "")
 669	uassert.NoError(t, err)
 670	fl, err := r.Register(alice, 1, "first", RPC, "https://flagged.example.com", "")
 671	uassert.NoError(t, err)
 672	uassert.NoError(t, r.ReviewEndpoint(fl, Flagged, zr(r, fl), erev(r, fl), curator, 1, "spam"))
 673	uassert.NoError(t, r.ReviewZone("first", Rejected, rev(r, "first"), curator, 2, "dup"))
 674	for i := 0; i < MaxRejected; i++ {
 675		slug := "rej" + strconv.Itoa(i)
 676		uassert.NoError(t, r.Propose(testAddr(i), 3, slug, onyx()))
 677		uassert.NoError(t, r.ReviewZone(slug, Rejected, rev(r, slug), curator, 4, "spam"))
 678	}
 679	_, ok := r.Zone("first")
 680	uassert.False(t, ok)
 681	_, total := r.Count("first")
 682	uassert.Equal(t, 0, total)
 683	uassert.Equal(t, 0, r.OwnerCount("first", alice))
 684	uassert.Equal(t, 0, r.EndpointCount("first", RPC))
 685	uassert.NoError(t, r.Propose(alice, 5, "first", onyx()))
 686	// Neither the review queue nor the flag count of the evicted zone carries
 687	// over to the slug proposed again.
 688	uassert.Equal(t, 0, r.Awaiting("first"))
 689	_, err = r.Register(alice, 5, "first", RPC, "https://f.example.com", "")
 690	uassert.NoError(t, err)
 691	uassert.Equal(t, 1, r.Awaiting("first"))
 692}
 693
 694// Approving a rejected or retired zone back into the live registry respects
 695// its cap.
 696func TestReviveRespectsTheLiveCap(t *testing.T) {
 697	r := NewRegistry()
 698	uassert.NoError(t, r.Propose(alice, 1, "back", onyx()))
 699	uassert.NoError(t, r.ReviewZone("back", Rejected, rev(r, "back"), curator, 1, "not yet"))
 700	for i := 0; i < MaxZones; i++ {
 701		slug := "z" + strconv.Itoa(i)
 702		uassert.NoError(t, r.ProposeExempt(curator, 2, slug, onyx()))
 703		uassert.NoError(t, r.ReviewZone(slug, Approved, rev(r, slug), curator, 2, ""))
 704	}
 705	uassert.Equal(t, MaxZones, r.Live())
 706	uassert.ErrorContains(t, r.ReviewZone("back", Approved, rev(r, "back"), curator, 3, ""), "full at")
 707	uassert.ErrorContains(t, r.Propose(bob, 3, "more", onyx()), "full at")
 708	uassert.NoError(t, r.ReviewZone("z0", Retired, rev(r, "z0"), curator, 3, "stopped"))
 709	uassert.NoError(t, r.ReviewZone("back", Approved, rev(r, "back"), curator, 4, ""))
 710}
 711
 712func TestUppercaseAddressesAreRefused(t *testing.T) {
 713	r := NewRegistry()
 714	upper := address(strings.ToUpper(alice.String()))
 715	uassert.True(t, upper.IsValid(), "bech32 itself decodes the uppercase form")
 716	uassert.False(t, ValidAddress(upper))
 717	uassert.ErrorContains(t, r.Propose(upper, 1, "onyx", onyx()), "lowercase")
 718	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
 719	_, err := r.Register(upper, 1, "onyx", RPC, "https://a.example.com", "")
 720	uassert.ErrorContains(t, err, "lowercase")
 721	uassert.ErrorContains(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), upper, 2, ""), "lowercase")
 722}
 723
 724// A peer is stored lowercased whole, so the listed form is the one tm2 dials,
 725// and two spellings of one host are one peer.
 726func TestPeerStoredLowercase(t *testing.T) {
 727	r := NewRegistry()
 728	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
 729	id, err := r.Register(alice, 1, "onyx", Peer, nodeID+"@SEED-1.Onyx.example.com:26656", "")
 730	uassert.NoError(t, err)
 731	e, _ := r.Endpoint(id)
 732	uassert.Equal(t, nodeID+"@seed-1.onyx.example.com:26656", e.Address)
 733	_, err = r.Register(alice, 1, "onyx", Peer, nodeID+"@seed-1.onyx.example.com:26656", "")
 734	uassert.ErrorContains(t, err, "already lists that peer")
 735	got, ok := r.EndpointByAddress("onyx", Peer, nodeID+"@Seed-1.onyx.example.com:26656")
 736	uassert.True(t, ok)
 737	uassert.Equal(t, id, got.ID)
 738}
 739
 740// Rejecting sends what was verified on the proposal back to unverified: a
 741// rejected zone was never vouched for, so nothing on it reads as checked.
 742func TestRejectUnverifies(t *testing.T) {
 743	r := NewRegistry()
 744	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
 745	id, err := r.Register(alice, 1, "onyx", RPC, "https://a.example.com", "")
 746	uassert.NoError(t, err)
 747	uassert.NoError(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), curator, 2, ""))
 748	uassert.NoError(t, r.ReviewZone("onyx", Rejected, rev(r, "onyx"), curator, 3, "not a gno chain"))
 749	e, _ := r.Endpoint(id)
 750	uassert.Equal(t, string(Unverified), string(e.Status))
 751	uassert.Equal(t, ZoneRejected, e.Reason)
 752	uassert.Equal(t, curator.String(), e.ReviewedBy.String())
 753	v, _ := r.Count("onyx")
 754	uassert.Equal(t, 0, v)
 755}
 756
 757// Eviction goes by when a zone entered its state, not when it was proposed: a
 758// long-lived network retired today is not the next record to go.
 759func TestEvictionIsByEntryOrder(t *testing.T) {
 760	r := NewRegistry()
 761	uassert.NoError(t, r.Propose(curator, 1, "mainnet", onyx())) // proposed first
 762	uassert.NoError(t, r.ReviewZone("mainnet", Approved, rev(r, "mainnet"), curator, 1, ""))
 763	for i := 0; i < MaxRetired; i++ {
 764		slug := "ret" + strconv.Itoa(i)
 765		uassert.NoError(t, r.Propose(curator, 2, slug, onyx()))
 766		uassert.NoError(t, r.ReviewZone(slug, Approved, rev(r, slug), curator, 2, ""))
 767		uassert.NoError(t, r.ReviewZone(slug, Retired, rev(r, slug), curator, 3, "stopped"))
 768	}
 769	// Retiring mainnet now drops ret0, the zone retired longest ago.
 770	uassert.NoError(t, r.ReviewZone("mainnet", Retired, rev(r, "mainnet"), curator, 4, "end of an era"))
 771	_, ok := r.Zone("ret0")
 772	uassert.False(t, ok)
 773	// And the next retirement drops ret1, not mainnet.
 774	uassert.NoError(t, r.Propose(curator, 5, "next", onyx()))
 775	uassert.NoError(t, r.ReviewZone("next", Approved, rev(r, "next"), curator, 5, ""))
 776	uassert.NoError(t, r.ReviewZone("next", Retired, rev(r, "next"), curator, 6, "stopped"))
 777	_, ok = r.Zone("mainnet")
 778	uassert.True(t, ok)
 779	_, ok = r.Zone("ret1")
 780	uassert.False(t, ok)
 781}
 782
 783// Every decision on a zone names the revision it was made on.
 784func TestEveryZoneDecisionIsRevisionBound(t *testing.T) {
 785	r := NewRegistry()
 786	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
 787	old := rev(r, "onyx")
 788	edited := onyx()
 789	edited.Title = "Onyx, edited"
 790	uassert.NoError(t, r.Edit("onyx", old, edited, alice, 2, ""))
 791	uassert.ErrorContains(t, r.Edit("onyx", old, onyx(), curator, 3, ""), "changed since you read it")
 792	uassert.ErrorContains(t, r.ReviewZone("onyx", Rejected, old, curator, 3, "spam"), "changed since you read it")
 793	uassert.ErrorContains(t, r.RemoveZone("onyx", old), "changed since you read it")
 794	uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 3, ""))
 795	uassert.ErrorContains(t, r.ReviewZone("onyx", Retired, old, curator, 4, "gone"), "changed since you read it")
 796	uassert.NoError(t, r.ReviewZone("onyx", Retired, rev(r, "onyx"), curator, 4, "gone"))
 797}
 798
 799func TestCanonicalHostAndTextEdgeCases(t *testing.T) {
 800	r := NewRegistry()
 801	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
 802	// A peer's host loses its terminal dot, so the two spellings are one peer.
 803	_, err := r.Register(alice, 1, "onyx", Peer, nodeID+"@seed-1.onyx.example.com:26656", "")
 804	uassert.NoError(t, err)
 805	_, err = r.Register(alice, 1, "onyx", Peer, nodeID+"@seed-1.onyx.example.com.:26656", "")
 806	uassert.ErrorContains(t, err, "already lists that peer")
 807	// An IPv4 host with a terminal dot is refused: Go's dialer cannot use it.
 808	uassert.ErrorContains(t, ValidateEndpoint(RPC, "http://1.2.3.4.:26657"), "DNS name or an IPv4")
 809	uassert.ErrorContains(t, ValidateEndpoint(Peer, nodeID+"@1.2.3.4.:26656"), "DNS name or an IPv4")
 810	// A loopback or private host is a local zone's only.
 811	_, err = r.Register(alice, 1, "onyx", RPC, "http://127.0.0.1:26657", "")
 812	uassert.ErrorContains(t, err, "private or special-use")
 813	_, err = r.Register(alice, 1, "onyx", Peer, nodeID+"@10.1.2.3:26656", "")
 814	uassert.ErrorContains(t, err, "private or special-use")
 815	local := onyx()
 816	local.Kind, local.RPCURL = Local, "http://127.0.0.1:26657"
 817	uassert.NoError(t, r.Propose(alice, 1, "mine", local))
 818	_, err = r.Register(alice, 1, "mine", Peer, nodeID+"@127.0.0.1:26656", "")
 819	uassert.NoError(t, err)
 820	// Equivalent URL spellings are one endpoint.
 821	for _, pair := range [][2]string{
 822		{"https://h.example.com/path", "https://h.example.com/path?"},
 823		{"https://h.example.com?a=1", "https://h.example.com/?a=1"},
 824		{"https://h.example.com/%2F", "https://h.example.com/%2f"},
 825	} {
 826		uassert.Equal(t, Canonical(Indexer, pair[0]), Canonical(Indexer, pair[1]), pair[0])
 827	}
 828	uassert.ErrorContains(t, ValidateEndpoint(Indexer, "https://h.example.com/%7e"), "needs no escaping")
 829	uassert.ErrorContains(t, ValidateEndpoint(Indexer, "https://h.example.com/%41"), "needs no escaping")
 830	// Look-alike badges and characters that draw nothing.
 831	in := onyx()
 832	for _, title := range []string{"✔ official", "☑ mainnet", "\U0001D159", "main\U000E0100net", "x\ue000", "\u180b", "\u0378"} {
 833		in.Title = title
 834		uassert.Error(t, ValidateInfo(in), title)
 835	}
 836}
 837
 838// The per-proposer and per-registrant caps are counts, and they follow every
 839// way a zone or an endpoint leaves.
 840func TestCountersFollowRemovals(t *testing.T) {
 841	r := NewRegistry()
 842	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
 843	a, _ := r.Register(alice, 1, "onyx", RPC, "https://a.example.com", "")
 844	_, _ = r.Register(alice, 1, "onyx", RPC, "https://b.example.com", "")
 845	uassert.Equal(t, 2, r.OwnerCount("onyx", alice))
 846	uassert.NoError(t, r.RemoveEndpoint(a, erev(r, a)))
 847	uassert.Equal(t, 1, r.OwnerCount("onyx", alice))
 848	uassert.NoError(t, r.RemoveZone("onyx", rev(r, "onyx")))
 849	uassert.Equal(t, 0, r.OwnerCount("onyx", alice))
 850	for i := 0; i < MaxPendingPerProposer; i++ {
 851		uassert.NoError(t, r.Propose(alice, 2, "p"+strconv.Itoa(i), onyx()))
 852	}
 853	uassert.ErrorContains(t, r.Propose(alice, 2, "over", onyx()), "pending proposals")
 854	uassert.NoError(t, r.ReviewZone("p0", Approved, rev(r, "p0"), curator, 3, ""))
 855	uassert.NoError(t, r.Propose(alice, 3, "over", onyx()))
 856}
 857
 858func TestSoleApproved(t *testing.T) {
 859	r := NewRegistry()
 860	uassert.NoError(t, r.Propose(curator, 1, "za", onyx()))
 861	_, ok := r.SoleApproved("onyx-1")
 862	uassert.False(t, ok, "pending is not approved")
 863	uassert.NoError(t, r.ReviewZone("za", Approved, rev(r, "za"), curator, 1, ""))
 864	z, ok := r.SoleApproved("onyx-1")
 865	uassert.True(t, ok)
 866	uassert.Equal(t, "za", z.Slug)
 867	uassert.NoError(t, r.Propose(curator, 1, "zb", onyx()))
 868	uassert.NoError(t, r.ReviewZone("zb", Approved, rev(r, "zb"), curator, 1, ""))
 869	_, ok = r.SoleApproved("onyx-1")
 870	uassert.False(t, ok, "two approved zones share it: no guess")
 871}
 872
 873// A status change bumps the revision too: an approval prepared before a
 874// colleague's rejection fails instead of quietly reversing it.
 875func TestStatusChangeBumpsTheRevision(t *testing.T) {
 876	r := NewRegistry()
 877	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
 878	read := rev(r, "onyx")
 879	uassert.NoError(t, r.ReviewZone("onyx", Rejected, read, bob, 2, "phishing RPC"))
 880	uassert.ErrorContains(t, r.ReviewZone("onyx", Approved, read, curator, 3, ""), "changed since you read it")
 881	z, _ := r.Zone("onyx")
 882	uassert.Equal(t, "phishing RPC", z.Reason)
 883}
 884
 885// Leaving the local kind drops the private endpoints in the same edit, so
 886// nobody can block the edit by registering one again after each removal.
 887func TestLeavingLocalDropsPrivateEndpoints(t *testing.T) {
 888	r := NewRegistry()
 889	local := onyx()
 890	local.Kind, local.RPCURL = Local, "http://127.0.0.1:26657"
 891	uassert.NoError(t, r.Propose(alice, 1, "mine", local))
 892	priv, err := r.Register(alice, 1, "mine", Peer, nodeID+"@10.0.0.5:26656", "")
 893	uassert.NoError(t, err)
 894	flagged, err := r.Register(bob, 1, "mine", RPC, "http://localhost:1", "")
 895	uassert.NoError(t, err)
 896	uassert.NoError(t, r.ReviewEndpoint(flagged, Flagged, zr(r, flagged), erev(r, flagged), curator, 1, "squatting"))
 897	pub, err := r.Register(alice, 1, "mine", RPC, "https://rpc.mine.example.com", "")
 898	uassert.NoError(t, err)
 899	// A ruled one is a record: the edit refuses to drop it unseen, and a
 900	// curator removes it first.
 901	uassert.ErrorContains(t, r.Edit("mine", rev(r, "mine"), onyx(), alice, 2, ""), "which a curator ruled on")
 902	uassert.NoError(t, r.RemoveEndpoint(flagged, erev(r, flagged)))
 903	uassert.NoError(t, r.Edit("mine", rev(r, "mine"), onyx(), alice, 2, ""))
 904	for _, id := range []int64{priv, flagged} {
 905		_, ok := r.Endpoint(id)
 906		uassert.False(t, ok, "a private endpoint is dropped")
 907	}
 908	_, ok := r.Endpoint(pub)
 909	uassert.True(t, ok, "a public one stays")
 910	uassert.Equal(t, 1, r.EndpointCount("mine", ""))
 911	uassert.Equal(t, 0, r.OwnerCount("mine", bob))
 912	uassert.Equal(t, 1, r.Awaiting("mine"))
 913}
 914
 915// A curator's chain-id edit of a pending zone names the curator on the resets;
 916// only the proposer's own edit records nobody.
 917func TestResetNamesACuratorEditor(t *testing.T) {
 918	r := NewRegistry()
 919	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
 920	id, _ := r.Register(alice, 1, "onyx", RPC, "https://a.example.com", "")
 921	uassert.NoError(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), curator, 2, ""))
 922	moved := onyx()
 923	moved.ChainID = "onyx-2"
 924	uassert.NoError(t, r.Edit("onyx", rev(r, "onyx"), moved, curator, 3, ""))
 925	e, _ := r.Endpoint(id)
 926	uassert.Equal(t, curator.String(), e.ReviewedBy.String())
 927}
 928
 929func TestPrivateHostsAndURLForms(t *testing.T) {
 930	for _, h := range []string{"validator", "node", "printer.local", "metadata.google.internal", "router.home.arpa",
 931		"foo.localdomain", "a.localhost", "192.0.0.1", "198.18.0.1", "198.19.255.255", "224.0.0.1", "255.255.255.255",
 932		"127.1", "0x7f.1", "2130706433",
 933		"rpc.test", "node.lan", "nas.home", "dc.corp", "wiki.intranet", "box.private", "x.onion", "y.alt",
 934		"192.0.2.1", "198.51.100.7", "203.0.113.255",
 935		"gno.example", "x.invalid", "rpc.mail", "ipv4only.arpa", "gnoland.default.service.arpa", "x.arpa",
 936		"100.127.255.255", "172.16.0.1", "172.31.255.255", "svc.service.consul", "node.lxd", "x.docker", "y.localnet"} {
 937		uassert.True(t, IsPrivateHost(h), h)
 938	}
 939	for _, h := range []string{"rpc.gno.land", "1.1.1.1", "198.20.0.1", "172.32.0.1", "172.15.255.255", "gno.land.", "100.128.0.1",
 940		"a.latest", "my.salt", "192.0.3.1", "198.51.101.1", "203.0.114.1",
 941		"example.com", "gmail.com", "arpa.example.com"} {
 942		uassert.False(t, IsPrivateHost(h), h)
 943	}
 944	// tcp:// and http:// are one rpc endpoint, the way gnokey dials them.
 945	uassert.Equal(t, Canonical(RPC, "tcp://h.example.com:26657"), Canonical(RPC, "http://h.example.com:26657"))
 946	uassert.Equal(t, Canonical(RPC, "tcp://h.example.com:80"), Canonical(RPC, "http://h.example.com"))
 947	// An empty query's ? goes; a query ending in ? keeps it.
 948	uassert.Equal(t, "https://h.example.com/p?a?", Canonical(Indexer, "https://h.example.com/p?a?"))
 949	// . and .. path segments are another spelling: refused.
 950	for _, u := range []string{"https://h.example.com/.", "https://h.example.com/./x", "https://h.example.com/a/..", "https://h.example.com/../a"} {
 951		uassert.ErrorContains(t, ValidateEndpoint(Indexer, u), "path segment", u)
 952	}
 953	uassert.NoError(t, ValidateEndpoint(Indexer, "https://h.example.com/a.b/..c/x."))
 954	in := onyx()
 955	for _, title := range []string{"⌛ pending", "❎ rejected", "\U0001F6D1 stop", "☒ no"} {
 956		in.Title = title
 957		uassert.ErrorContains(t, ValidateInfo(in), "status glyph", title)
 958	}
 959}
 960
 961// A copied Registry value is the same registry: it shares the revision
 962// counter with the zones it shares, so no revision is handed out twice. With
 963// the counter inline, an edit through the copy and then one through the
 964// original would both produce the same revision number, and a decision made on
 965// the first content would pass against the second.
 966func TestCopiedRegistryIsTheSameRegistry(t *testing.T) {
 967	r1 := NewRegistry()
 968	uassert.NoError(t, r1.Propose(alice, 1, "onyx", onyx()))
 969	r2 := *r1
 970	in := onyx()
 971	in.Title = "via the copy"
 972	uassert.NoError(t, r2.Edit("onyx", rev(r1, "onyx"), in, alice, 2, ""))
 973	seen := rev(r1, "onyx") // what a curator read: the copy's content
 974	in.Title = "via the original"
 975	uassert.NoError(t, r1.Edit("onyx", seen, in, alice, 3, ""))
 976	uassert.True(t, rev(r1, "onyx") != seen, "a new content, a new revision")
 977	uassert.ErrorContains(t, r1.ReviewZone("onyx", Approved, seen, curator, 4, ""), "changed since you read it")
 978}
 979
 980// Editing a local zone that stays local drops nothing, and every Info URL is
 981// checked for a private host, not only the main RPC.
 982func TestOnlyLeavingLocalDrops(t *testing.T) {
 983	r := NewRegistry()
 984	local := onyx()
 985	local.Kind, local.RPCURL = Local, "http://127.0.0.1:26657"
 986	uassert.NoError(t, r.Propose(alice, 1, "mine", local))
 987	id, err := r.Register(alice, 1, "mine", Peer, nodeID+"@10.0.0.5:26656", "")
 988	uassert.NoError(t, err)
 989	local.Title = "Mine, renamed"
 990	uassert.NoError(t, r.Edit("mine", rev(r, "mine"), local, alice, 2, ""))
 991	_, ok := r.Endpoint(id)
 992	uassert.True(t, ok, "a local zone keeps its private endpoints")
 993
 994	for _, set := range []func(*Info){
 995		func(in *Info) { in.GnowebURL = "http://192.168.1.2" },
 996		func(in *Info) { in.GenesisURL = "https://files.lan/genesis.json" },
 997	} {
 998		in := onyx()
 999		set(&in)
1000		uassert.ErrorContains(t, ValidateInfo(in), "private or special-use")
1001	}
1002}
1003
1004// Any edit to an approved zone is a review on record, so a chain-id reset it
1005// causes names its editor, whoever that is; only a pending zone's own
1006// proposer resets as nobody.
1007func TestApprovedEditResetNamesTheEditor(t *testing.T) {
1008	r := NewRegistry()
1009	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1010	uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 2, ""))
1011	id, _ := r.Register(bob, 3, "onyx", RPC, "https://a.example.com", "")
1012	uassert.NoError(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), curator, 4, ""))
1013	moved := onyx()
1014	moved.ChainID = "onyx-2"
1015	uassert.NoError(t, r.Edit("onyx", rev(r, "onyx"), moved, alice, 5, "relaunched"))
1016	e, _ := r.Endpoint(id)
1017	uassert.Equal(t, alice.String(), e.ReviewedBy.String())
1018}
1019
1020// A curator's exemption is from the review queue, never from the live cap.
1021func TestExemptStillMeetsTheLiveCap(t *testing.T) {
1022	r := NewRegistry()
1023	for i := 0; i < MaxZones; i++ {
1024		slug := "z" + strconv.Itoa(i)
1025		uassert.NoError(t, r.ProposeExempt(curator, 1, slug, onyx()))
1026		uassert.NoError(t, r.ReviewZone(slug, Approved, rev(r, slug), curator, 1, ""))
1027	}
1028	uassert.ErrorContains(t, r.ProposeExempt(curator, 2, "one-more", onyx()), "the registry is full")
1029}
1030
1031// The URL forms each kind dials: a tcp:// rpc is host and port only, an
1032// indexer may be a websocket, and IsPrivateHost fails closed on anything but a
1033// bare host.
1034func TestURLFormsPerKind(t *testing.T) {
1035	uassert.ErrorContains(t, ValidateEndpoint(RPC, "tcp://rpc.example.com:26657/websocket"), "host and port only")
1036	uassert.ErrorContains(t, ValidateEndpoint(RPC, "tcp://rpc.example.com:26657?x=1"), "host and port only")
1037	uassert.NoError(t, ValidateEndpoint(RPC, "tcp://rpc.example.com:26657"))
1038	uassert.NoError(t, ValidateEndpoint(RPC, "wss://rpc.example.com/websocket"))
1039	uassert.NoError(t, ValidateEndpoint(Indexer, "wss://indexer.example.com/graphql/query"))
1040	uassert.Error(t, ValidateEndpoint(Faucet, "wss://faucet.example.com"))
1041	for _, h := range []string{"127.0.0.1:26657", "10.0.0.1/", "foo.local:80", "a b", "192.88.99.1"} {
1042		uassert.True(t, IsPrivateHost(h), h)
1043	}
1044}
1045
1046// A rejection or a retirement is restated with a new reason, the only way to
1047// correct one, without a second eviction or reset.
1048func TestARejectionReasonCanBeRestated(t *testing.T) {
1049	r := NewRegistry()
1050	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1051	uassert.NoError(t, r.ReviewZone("onyx", Rejected, rev(r, "onyx"), curator, 2, "wrong netwrok"))
1052	entered := func() int64 { z, _ := r.Zone("onyx"); return z.Entered }
1053	before := entered()
1054	uassert.NoError(t, r.ReviewZone("onyx", Rejected, rev(r, "onyx"), bob, 3, "wrong network"))
1055	z, _ := r.Zone("onyx")
1056	uassert.Equal(t, "wrong network", z.Reason)
1057	uassert.Equal(t, bob.String(), z.ReviewedBy.String())
1058	uassert.Equal(t, before, entered(), "a restatement is not a new entry")
1059	uassert.ErrorContains(t, r.ReviewZone("onyx", Rejected, rev(r, "onyx"), bob, 4, "wrong network"), "already rejected")
1060}
1061
1062// Flagging a verified endpoint puts it back in the unchecked index, so counts
1063// and a later reset see it as what it is.
1064func TestFlaggingAVerifiedEndpointUnchecksIt(t *testing.T) {
1065	r := NewRegistry()
1066	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1067	uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 1, ""))
1068	id, _ := r.Register(bob, 2, "onyx", RPC, "https://a.example.com", "")
1069	uassert.NoError(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), curator, 3, ""))
1070	uassert.NoError(t, r.ReviewEndpoint(id, Flagged, zr(r, id), erev(r, id), carol, 4, "down"))
1071	v, total := r.Count("onyx")
1072	uassert.Equal(t, 0, v)
1073	uassert.Equal(t, 1, total)
1074	uassert.Equal(t, 0, r.Awaiting("onyx"))
1075	// A retirement then keeps the flag: it resets only what is verified.
1076	uassert.NoError(t, r.ReviewZone("onyx", Retired, rev(r, "onyx"), curator, 5, "shut down"))
1077	e, _ := r.Endpoint(id)
1078	uassert.Equal(t, string(Flagged), string(e.Status))
1079	// A restated verdict names its new reviewer.
1080	uassert.NoError(t, r.ReviewEndpoint(id, Flagged, zr(r, id), erev(r, id), curator, 6, "down for good"))
1081	e, _ = r.Endpoint(id)
1082	uassert.Equal(t, curator.String(), e.ReviewedBy.String())
1083}
1084
1085// A reset moves the endpoint's revision, so a verdict written against the
1086// verified endpoint fails after a retirement reset it.
1087func TestAResetMovesTheEndpointRevision(t *testing.T) {
1088	r := NewRegistry()
1089	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1090	uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 1, ""))
1091	id, _ := r.Register(bob, 2, "onyx", RPC, "https://a.example.com", "")
1092	uassert.NoError(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), curator, 3, ""))
1093	read := erev(r, id)
1094	uassert.NoError(t, r.ReviewZone("onyx", Retired, rev(r, "onyx"), curator, 4, "shut down"))
1095	uassert.ErrorContains(t, r.ReviewEndpoint(id, Flagged, zr(r, id), read, carol, 5, "x"), "changed since")
1096}
1097
1098// The package README's example, verbatim but for must and the names: it is
1099// the first thing an importer copies, so it is run.
1100func TestTheReadmeExampleRuns(t *testing.T) {
1101	proposer, height := alice, int64(1)
1102	r := NewRegistry()
1103	uassert.NoError(t, r.Propose(proposer, height, "onyx", Info{ChainID: "onyx-1", Title: "Onyx",
1104		Kind: Testnet, RPCURL: "https://rpc.onyx.testnets.gno.land"}))
1105	z, _ := r.Zone("onyx")
1106	uassert.NoError(t, r.ReviewZone("onyx", Approved, z.Revision, curator, height, ""))
1107	z, _ = r.Zone("onyx")
1108	id, err := r.Register(proposer, height, "onyx", Peer,
1109		"g1x5mlj5ava0dw9vkf4j6admjlzswm6f06p44krn@seed-1.onyx.testnets.gno.land:26656", "gno core")
1110	uassert.NoError(t, err)
1111	e, _ := r.Endpoint(id)
1112	uassert.NoError(t, r.ReviewEndpoint(id, Verified, z.Revision, e.Revision, curator, height, "answers onyx-1"))
1113	uassert.Equal(t, 1, len(r.Endpoints(EndpointFilter{Zone: "onyx", Kind: Peer, Status: Verified})))
1114}
1115
1116// A verification restated with a new reason names its new reviewer, and a
1117// zone removed and proposed again starts with no private endpoints on record.
1118func TestRestatedVerifyAndARemovedZonesPrivateIndex(t *testing.T) {
1119	r := NewRegistry()
1120	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1121	id, _ := r.Register(alice, 1, "onyx", RPC, "https://a.example.com", "")
1122	uassert.NoError(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), curator, 2, "answers"))
1123	uassert.NoError(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), carol, 3, "answers, re-probed"))
1124	e, _ := r.Endpoint(id)
1125	uassert.Equal(t, carol.String(), e.ReviewedBy.String())
1126
1127	local := onyx()
1128	local.Kind, local.RPCURL = Local, "http://127.0.0.1:26657"
1129	uassert.NoError(t, r.Propose(alice, 4, "lab", local))
1130	_, err := r.Register(alice, 4, "lab", RPC, "http://10.0.0.1:26657", "")
1131	uassert.NoError(t, err)
1132	uassert.NoError(t, r.RemoveZone("lab", rev(r, "lab")))
1133	uassert.NoError(t, r.Propose(alice, 5, "lab", local))
1134	uassert.Equal(t, 0, len(r.PrivateEndpoints("lab")))
1135	uassert.NoError(t, r.Edit("lab", rev(r, "lab"), onyx(), alice, 6, ""))
1136}
1137
1138// A zone decision naming a revision from the future fails as a stale one does.
1139func TestAFutureRevisionIsRefused(t *testing.T) {
1140	r := NewRegistry()
1141	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1142	uassert.ErrorContains(t, r.ReviewZone("onyx", Approved, rev(r, "onyx")+1, curator, 2, ""), "changed since you read it")
1143}
1144
1145// Restating: any review state with a new visible reason, bumping the
1146// revision; never with an empty or the same reason.
1147func TestRestatementRules(t *testing.T) {
1148	r := NewRegistry()
1149	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1150	uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 2, "answers onyx-1"))
1151	read := rev(r, "onyx")
1152	uassert.NoError(t, r.ReviewZone("onyx", Approved, read, carol, 3, "answers onyx-1, re-probed"))
1153	uassert.True(t, rev(r, "onyx") > read, "a restatement moves the revision")
1154	uassert.ErrorContains(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), carol, 4, " "), "already approved")
1155	id, _ := r.Register(bob, 4, "onyx", RPC, "https://a.example.com", "")
1156	uassert.NoError(t, r.ReviewEndpoint(id, Flagged, zr(r, id), erev(r, id), curator, 4, "down"))
1157	uassert.ErrorContains(t, r.ReviewEndpoint(id, Flagged, zr(r, id), erev(r, id), curator, 4, ""), "needs a new reason")
1158	uassert.NoError(t, r.ReviewZone("onyx", Retired, rev(r, "onyx"), curator, 5, "shut down"))
1159	uassert.NoError(t, r.ReviewZone("onyx", Retired, rev(r, "onyx"), carol, 6, "shut down 2026-10-01"))
1160	z, _ := r.Zone("onyx")
1161	uassert.Equal(t, "shut down 2026-10-01", z.Reason)
1162
1163}
1164
1165// What a URL is stored as: scheme and host lowercased in ASCII only, the path
1166// and query as typed; a letter that lowercases into ASCII is refused, not
1167// folded. A tcp:// rpc path is refused whatever the scheme's case.
1168func TestStoredURLForm(t *testing.T) {
1169	r := NewRegistry()
1170	in := onyx()
1171	in.GenesisURL = "HTTPS://Files.Example.com/G?Q=1"
1172	uassert.NoError(t, r.Propose(alice, 1, "onyx", in))
1173	z, _ := r.Zone("onyx")
1174	uassert.Equal(t, "https://files.example.com/G?Q=1", z.GenesisURL)
1175	id, err := r.Register(alice, 1, "onyx", Indexer, "HTTPS://Idx.Example.com?Q=1", "")
1176	uassert.NoError(t, err)
1177	e, _ := r.Endpoint(id)
1178	uassert.Equal(t, "https://idx.example.com?Q=1", e.Address)
1179	for _, u := range []string{"https://Key.example.com", "https://rpc.İnfo.example.com"} {
1180		bad := onyx()
1181		bad.RPCURL = u
1182		uassert.Error(t, r.Propose(bob, 2, "x"+strconv.Itoa(len(u)), bad), u)
1183	}
1184	uassert.ErrorContains(t, ValidateEndpoint(RPC, "TCP://h.example.com:26657/websocket"), "host and port only")
1185	uassert.False(t, IsPrivateHost("RPC.GNO.LAND"))
1186	uassert.ErrorContains(t, ValidateEndpoint(RPC, "https://café.example.com"), "'é'")
1187}
1188
1189// A curator's leave-local edit is refused while a private endpoint carries a
1190// ruling of any kind: a reset's reason, a reasonless unverify, a verification;
1191// a curator who proposed the zone included.
1192func TestLeavingLocalRefusesEveryRuling(t *testing.T) {
1193	setup := func() (*Registry, int64) {
1194		r := NewRegistry()
1195		local := onyx()
1196		local.Kind, local.RPCURL = Local, "http://127.0.0.1:26657"
1197		uassert.NoError(t, r.Propose(curator, 1, "lab", local))
1198		id, err := r.Register(curator, 1, "lab", RPC, "http://10.0.0.1:26657", "")
1199		uassert.NoError(t, err)
1200		return r, id
1201	}
1202	r, id := setup()
1203	uassert.NoError(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), carol, 2, ""))
1204	uassert.ErrorContains(t, r.Edit("lab", rev(r, "lab"), onyx(), curator, 3, ""), "is verified")
1205
1206	r, id = setup()
1207	uassert.NoError(t, r.ReviewEndpoint(id, Flagged, zr(r, id), erev(r, id), carol, 2, "x"))
1208	uassert.NoError(t, r.ReviewEndpoint(id, Unverified, zr(r, id), erev(r, id), carol, 3, ""))
1209	uassert.ErrorContains(t, r.Edit("lab", rev(r, "lab"), onyx(), curator, 4, ""), "a curator ruled on")
1210
1211	r, id = setup()
1212	uassert.NoError(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), carol, 2, ""))
1213	moved := onyx()
1214	moved.Kind, moved.RPCURL, moved.ChainID = Local, "http://127.0.0.1:26657", "lab-2"
1215	uassert.NoError(t, r.Edit("lab", rev(r, "lab"), moved, curator, 3, "")) // the proposer's reset: no reviewer, a reason
1216	e, _ := r.Endpoint(id)
1217	uassert.Equal(t, "", e.ReviewedBy.String())
1218	uassert.ErrorContains(t, r.Edit("lab", rev(r, "lab"), onyx(), curator, 4, ""), "a curator ruled on")
1219}
1220
1221// Clearable counts what a bulk clear may remove: registered through the gate,
1222// never ruled on. A first verdict or a removal takes one out (a reset cannot:
1223// it touches only verified endpoints); an exempt registration never counts.
1224func TestTheClearableCount(t *testing.T) {
1225	r := NewRegistry()
1226	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1227	uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 1, ""))
1228	a, _ := r.Register(bob, 2, "onyx", RPC, "https://a.example.com", "")
1229	b, _ := r.Register(bob, 2, "onyx", RPC, "https://b.example.com", "")
1230	c, _ := r.Register(bob, 2, "onyx", RPC, "https://c.example.com", "")
1231	x, _ := r.RegisterExempt(curator, 2, "onyx", RPC, "https://x.example.com", "")
1232	ex, _ := r.Endpoint(x)
1233	uassert.True(t, ex.Exempt)
1234	uassert.False(t, ex.Clearable())
1235	uassert.Equal(t, 3, r.Clearable("onyx"))
1236	uassert.NoError(t, r.ReviewEndpoint(a, Verified, zr(r, a), erev(r, a), curator, 3, ""))
1237	uassert.Equal(t, 2, r.Clearable("onyx"))
1238	uassert.NoError(t, r.ReviewEndpoint(a, Flagged, zr(r, a), erev(r, a), curator, 3, "down"))
1239	uassert.Equal(t, 2, r.Clearable("onyx"), "a second verdict changes nothing")
1240	uassert.NoError(t, r.RemoveEndpoint(b, erev(r, b)))
1241	uassert.Equal(t, 1, r.Clearable("onyx"))
1242	moved := onyx()
1243	moved.ChainID = "onyx-2"
1244	uassert.NoError(t, r.Edit("onyx", rev(r, "onyx"), moved, curator, 4, "relaunch"))
1245	uassert.Equal(t, 1, r.Clearable("onyx"), "a reset touches only verified endpoints")
1246	_ = c
1247}
1248
1249// One edit off the local kind drops at most MaxDropPerEdit endpoints.
1250func TestLeavingLocalDropsAtMostTheCap(t *testing.T) {
1251	r := NewRegistry()
1252	local := onyx()
1253	local.Kind, local.RPCURL = Local, "http://127.0.0.1:26657"
1254	uassert.NoError(t, r.Propose(curator, 1, "lab", local))
1255	for i := 0; i <= MaxDropPerEdit; i++ {
1256		_, err := r.RegisterExempt(curator, 1, "lab", RPC, "http://10.0.0."+strconv.Itoa(i%250+1)+":"+strconv.Itoa(26000+i), "")
1257		uassert.NoError(t, err)
1258	}
1259	uassert.ErrorContains(t, r.Edit("lab", rev(r, "lab"), onyx(), curator, 2, ""), "more than 64 in one edit")
1260	ids := r.PrivateEndpoints("lab")
1261	uassert.NoError(t, r.RemoveEndpoint(ids[0].ID, ids[0].Revision))
1262	uassert.NoError(t, r.Edit("lab", rev(r, "lab"), onyx(), curator, 3, ""))
1263}
1264
1265// The numbers the READMEs state, pinned: a change to one is a change to what
1266// the docs promise.
1267func TestTheDocumentedBounds(t *testing.T) {
1268	for name, got := range map[string]int{"MaxZones": MaxZones, "MaxPending": MaxPending, "MaxPendingPerProposer": MaxPendingPerProposer,
1269		"MaxRejected": MaxRejected, "MaxRetired": MaxRetired, "MaxEndpointsPerZone": MaxEndpointsPerZone,
1270		"MaxUnverifiedPerZone": MaxUnverifiedPerZone, "MaxEndpointsPerAddress": MaxEndpointsPerAddress, "MaxDropPerEdit": MaxDropPerEdit,
1271		"MaxTitleLen": MaxTitleLen, "MaxDescriptionLen": MaxDescriptionLen, "MaxLabelLen": MaxLabelLen, "MaxReasonLen": MaxReasonLen, "MaxURLLen": MaxURLLen} {
1272		want := map[string]int{"MaxZones": 256, "MaxPending": 64, "MaxPendingPerProposer": 4, "MaxRejected": 64, "MaxRetired": 128,
1273			"MaxEndpointsPerZone": 128, "MaxUnverifiedPerZone": 64, "MaxEndpointsPerAddress": 16, "MaxDropPerEdit": 64,
1274			"MaxTitleLen": 64, "MaxDescriptionLen": 512, "MaxLabelLen": 64, "MaxReasonLen": 280, "MaxURLLen": 256}[name]
1275		uassert.Equal(t, want, got, name)
1276	}
1277}
1278
1279// Rules the other tests reach only in part.
1280func TestValidationEdges(t *testing.T) {
1281	// A private host is refused on every kind but local.
1282	for _, k := range []Kind{Mainnet, Testnet, Devnet} {
1283		in := onyx()
1284		in.Kind, in.RPCURL = k, "http://10.0.0.1:26657"
1285		uassert.ErrorContains(t, ValidateInfo(in), "private or special-use", string(k))
1286	}
1287	// The combining grapheme joiner draws nothing.
1288	in := onyx()
1289	in.Title = "On\u034fyx"
1290	uassert.ErrorContains(t, ValidateInfo(in), "invisible")
1291	// Needless escapes of unreserved characters, and an entity shape.
1292	for _, u := range []string{"https://h.example.com/a%5Fb", "https://h.example.com/a%2Db", "https://h.example.com/%2E%2E/x",
1293		"https://h.example.com/a&#38;b"} {
1294		uassert.Error(t, ValidateEndpoint(RPC, u), u)
1295	}
1296	// Dot segments are a path rule; a query may hold dots.
1297	uassert.NoError(t, ValidateEndpoint(Explorer, "https://h.example.com/p?x=../y"))
1298	// Default websocket ports are dropped like the http ones.
1299	uassert.Equal(t, Canonical(RPC, "wss://h.example.com/ws"), Canonical(RPC, "wss://h.example.com:443/ws"))
1300	uassert.Equal(t, Canonical(RPC, "ws://h.example.com/ws"), Canonical(RPC, "ws://h.example.com:80/ws"))
1301}
1302
1303// What a caller types is trimmed before it is used: the kind, a label, a
1304// reason; and a restatement of verify or unverify needs a new visible reason.
1305func TestTrimsAndRestatementReasons(t *testing.T) {
1306	r := NewRegistry()
1307	in := onyx()
1308	in.Kind = " testnet "
1309	uassert.NoError(t, r.Propose(alice, 1, "onyx", in))
1310	z, _ := r.Zone("onyx")
1311	uassert.Equal(t, string(Testnet), string(z.Kind))
1312	uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 1, ""))
1313	id, _ := r.Register(bob, 2, "onyx", RPC, "https://a.example.com", "  bob's node  ")
1314	e, _ := r.Endpoint(id)
1315	uassert.Equal(t, "bob's node", e.Label)
1316	uassert.NoError(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), curator, 3, "  answers  "))
1317	e, _ = r.Endpoint(id)
1318	uassert.Equal(t, "answers", e.Reason)
1319	uassert.ErrorContains(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), curator, 4, " "), "needs a new reason")
1320	uassert.NoError(t, r.ReviewEndpoint(id, Unverified, zr(r, id), erev(r, id), curator, 5, "x"))
1321	uassert.ErrorContains(t, r.ReviewEndpoint(id, Unverified, zr(r, id), erev(r, id), curator, 6, ""), "needs a new reason")
1322	edited := onyx()
1323	edited.Title = "Onyx, edited"
1324	uassert.NoError(t, r.Edit("onyx", rev(r, "onyx"), edited, curator, 7, "  retitled  "))
1325	z, _ = r.Zone("onyx")
1326	uassert.Equal(t, "retitled", z.Reason)
1327	uassert.Equal(t, rev(r, "onyx"), r.Revision(), "the last revision handed out")
1328}
1329
1330// A zone removed and proposed again starts with nothing clearable.
1331func TestARemovedZonesClearableCountIsGone(t *testing.T) {
1332	r := NewRegistry()
1333	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1334	_, err := r.Register(alice, 1, "onyx", RPC, "https://a.example.com", "")
1335	uassert.NoError(t, err)
1336	uassert.Equal(t, 1, r.Clearable("onyx"))
1337	uassert.NoError(t, r.RemoveZone("onyx", rev(r, "onyx")))
1338	uassert.NoError(t, r.Propose(alice, 2, "onyx", onyx()))
1339	uassert.Equal(t, 0, r.Clearable("onyx"))
1340}
1341
1342// URL characters, escapes, schemes, selectors, Parse trimming, pending
1343// restatement and stored tails.
1344func TestURLAndTextRules(t *testing.T) {
1345	// Every character a URL may not hold, one at a time.
1346	for _, c := range []string{"<", ">", "`", "(", ")", "[", "]", "{", "}", "|", "\\", "^", "#", "\"", "'"} {
1347		uassert.Error(t, ValidateEndpoint(RPC, "https://h.example.com/a"+c+"b"), c)
1348	}
1349	// A needless escape of a digit or a lowercase letter; the hex case of the
1350	// first escape digit.
1351	uassert.Error(t, ValidateEndpoint(RPC, "https://h.example.com/a%30"))
1352	uassert.Error(t, ValidateEndpoint(RPC, "https://h.example.com/a%61"))
1353	uassert.Equal(t, Canonical(RPC, "https://h.example.com/%af"), Canonical(RPC, "https://h.example.com/%AF"))
1354	// A hex last label reads as an IPv4 number.
1355	uassert.Error(t, ValidateEndpoint(RPC, "https://a.0x1"))
1356	uassert.True(t, IsPrivateHost("a.0x1"))
1357	uassert.True(t, IsPrivateHost("PRINTER.LOCAL"))
1358	// Schemes per field.
1359	in := onyx()
1360	in.GnowebURL = "tcp://onyx.example.com:26657"
1361	uassert.Error(t, ValidateInfo(in))
1362	uassert.Error(t, ValidateEndpoint(Indexer, "tcp://indexer.example.com:8546"))
1363	// Variation selectors only after a base they modify.
1364	for _, s := range []string{"a\ufe00", "᠀\u180b"} {
1365		in := onyx()
1366		in.Title = s
1367		uassert.ErrorContains(t, ValidateInfo(in), "invisible", s)
1368	}
1369	ok := onyx()
1370	ok.Title = "ᠨ\u180f"
1371	uassert.NoError(t, ValidateInfo(ok))
1372	uassert.False(t, HasVisible("\u200b\u200d"))
1373	// The Parse functions trim.
1374	st, err := ParseStatus(" approved ")
1375	uassert.NoError(t, err)
1376	uassert.Equal(t, string(Approved), string(st))
1377	k, err := ParseEndpointKind(" rpc ")
1378	uassert.NoError(t, err)
1379	uassert.Equal(t, string(RPC), string(k))
1380	v, err := ParseVerification(" verified ")
1381	uassert.NoError(t, err)
1382	uassert.Equal(t, string(Verified), string(v))
1383	// Nothing goes back to pending, not even as a restatement.
1384	r := NewRegistry()
1385	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1386	uassert.Error(t, r.ReviewZone("onyx", Pending, rev(r, "onyx"), curator, 2, "x"))
1387	z, _ := r.Zone("onyx")
1388	uassert.Equal(t, "", z.ReviewedBy.String())
1389	// An empty query and a bare "/" are not stored, so the dialable spelling
1390	// is the one listed.
1391	id, err := r.Register(alice, 3, "onyx", RPC, "https://rpc2.zz.example.com/?", "")
1392	uassert.NoError(t, err)
1393	e, _ := r.Endpoint(id)
1394	uassert.Equal(t, "https://rpc2.zz.example.com", e.Address)
1395	_, err = r.Register(alice, 3, "onyx", RPC, "https://rpc2.zz.example.com", "")
1396	uassert.ErrorContains(t, err, "already lists")
1397	// More private suffixes, more look-alikes.
1398	for _, h := range []string{"rpc.node.incus", "web.dns.podman", "foo.i2p", "gnoland.default.svc"} {
1399		uassert.True(t, IsPrivateHost(h), h)
1400	}
1401	for _, r := range []rune{0x1FBBD, 0x1F6AD, 0x1F6AF, 0x1F6B1, 0x1F6B3, 0x1F6B7, 0x1F4F5, 0x1F51E, 0x1F10D, 0x1F10F, 0x1F16E} {
1402		uassert.Error(t, ValidateLabel(string(r)+" official"), string(r))
1403	}
1404}
1405
1406// What is stored canonicalizes as what was typed, so a check made on the
1407// typed address (the realm's own-URL reservation) holds for the stored one.
1408func TestTheStoredFormKeepsItsCanonicalForm(t *testing.T) {
1409	for _, u := range []string{"https://h.example.com??", "https://h.example.com/??", "https://h.example.com?/",
1410		"https://h.example.com?path=/", "https://h.example.com/p?x=?", "https://h.example.com/p??", "https://h.example.com/?",
1411		"https://h.example.com/", "https://h.example.com?", "https://h.example.com/?q=1", "https://h.example.com/p/"} {
1412		uassert.Equal(t, Canonical(RPC, u), Canonical(RPC, trimEmptyTail(lowerAuthority(u))), u)
1413	}
1414	uassert.Equal(t, "https://h.example.com?path=/", trimEmptyTail("https://h.example.com?path=/"))
1415	uassert.Equal(t, "https://h.example.com??", trimEmptyTail("https://h.example.com??"))
1416}
1417
1418// Spaces of every kind (Unicode Zs) and tabs are trimmed; a line separator at
1419// an edge reaches the validator and is refused, never silently cut. A zone's
1420// gnoweb and genesis URLs have an empty tail repaired, its main RPC is refused
1421// with one, and an endpoint is validated as it will be stored.
1422func TestTrimsSpacesNotLineBreaks(t *testing.T) {
1423	r := NewRegistry()
1424	in := onyx()
1425	in.Title = "Onyx\u2028"
1426	uassert.ErrorContains(t, r.Propose(alice, 1, "onyx", in), "control character")
1427	in = onyx()
1428	in.Description = "\u0085" + in.Description
1429	uassert.ErrorContains(t, r.Propose(alice, 1, "onyx", in), "control character")
1430	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1431	_, err := r.Register(alice, 2, "onyx", RPC, "https://a.example.com", "operator\u2028")
1432	uassert.ErrorContains(t, err, "control character")
1433	uassert.Equal(t, "x", TrimSpaces(" \tx\t "))
1434	for _, u := range []string{"https://g.example.com/", "https://g.example.com?", "https://g.example.com/?"} {
1435		r := NewRegistry()
1436		in := onyx()
1437		in.GnowebURL, in.GenesisURL = u, u
1438		uassert.NoError(t, r.Propose(alice, 1, "xz", in), u)
1439		z, _ := r.Zone("xz")
1440		uassert.Equal(t, "https://g.example.com", z.GnowebURL, u)
1441		uassert.Equal(t, "https://g.example.com", z.GenesisURL, u)
1442	}
1443	for _, u := range []string{"https://rpc.example.com/", "https://rpc.example.com?"} {
1444		bad := onyx()
1445		bad.RPCURL = u
1446		uassert.Error(t, ValidateInfo(bad), u)
1447		uassert.Error(t, NewRegistry().Propose(alice, 1, "xz", bad), "the registry refuses it too: "+u)
1448	}
1449	uassert.Equal(t, "Onyx", TrimSpaces("\u00a0Onyx\u3000"))
1450	uassert.Equal(t, "Onyx\n", TrimSpaces("Onyx\n"))
1451	// ValidateEndpoint judges the string it is given; Register stores the
1452	// trimmed form and validates that.
1453	uassert.Error(t, ValidateEndpoint(RPC, "tcp://rpc.example.com:26657/"), "as given, it has a path")
1454	id, err := r.Register(alice, 2, "onyx", RPC, "tcp://rpc.example.com:26657/", "")
1455	uassert.NoError(t, err)
1456	e, _ := r.Endpoint(id)
1457	uassert.Equal(t, "tcp://rpc.example.com:26657", e.Address)
1458	long := "https://e.example.com/" + strings.Repeat("a", MaxURLLen-len("https://e.example.com/"))
1459	_, err = r.Register(alice, 2, "onyx", Explorer, long+"?", "")
1460	uassert.NoError(t, err, "257 bytes as typed, 256 as stored")
1461	_, err = r.Register(alice, 2, "onyx", Explorer, long+"b", "")
1462	uassert.Error(t, err, "257 as stored")
1463}
1464
1465// Chain id charset, text and URL bounds, controls, escapes and trims.
1466func TestMoreBoundaries(t *testing.T) {
1467	in := onyx()
1468	in.ChainID = "onyx:1"
1469	uassert.Error(t, ValidateInfo(in))
1470	uassert.Error(t, ValidateReason(strings.Repeat("x", MaxReasonLen+1)))
1471	uassert.NoError(t, ValidateReason(strings.Repeat("x", MaxReasonLen)))
1472	uassert.Error(t, ValidateLabel(strings.Repeat("x", MaxLabelLen+1)))
1473	uassert.NoError(t, ValidateLabel(strings.Repeat("x", MaxLabelLen)))
1474	for _, c := range []string{"\x1f", "\x7f", "\u009f"} {
1475		uassert.ErrorContains(t, ValidateLabel("a"+c+"b"), "control character")
1476	}
1477	ok := onyx()
1478	ok.Title = "ᠠ\u180b"
1479	uassert.NoError(t, ValidateInfo(ok))
1480	host := "https://" + strings.Repeat("a", 20) + ".example.com/"
1481	long := host + strings.Repeat("p", MaxURLLen-len(host))
1482	uassert.NoError(t, ValidateEndpoint(RPC, long))
1483	uassert.Error(t, ValidateEndpoint(RPC, long+"p"))
1484	uassert.Error(t, ValidateEndpoint(RPC, "https://h.example.com/a\x7fb"))
1485	uassert.NoError(t, ValidateEndpoint(Explorer, "https://h.example.com/a&;b"))
1486	uassert.Error(t, ValidateEndpoint(RPC, "https://h.example.com/a%2z"))
1487	uassert.NoError(t, ValidateEndpoint(Explorer, "https://h.example.com/p?a=/../"))
1488	uassert.Error(t, ValidateEndpoint(RPC, "https://a.0X1"))
1489	uassert.Error(t, ValidateEndpoint(RPC, "https://256.1.1.1"))
1490
1491	r := NewRegistry()
1492	in = onyx()
1493	in.Description = " \t" + in.Description + "\t "
1494	uassert.NoError(t, r.Propose(alice, 1, "onyx", in))
1495	z, _ := r.Zone("onyx")
1496	uassert.Equal(t, onyx().Description, z.Description)
1497	edited := onyx()
1498	edited.Title = "Onyx, edited"
1499	uassert.ErrorContains(t, r.Edit("onyx", rev(r, "onyx"), edited, alice, 2, "x"), "takes no reason")
1500}
1501
1502// Boundaries the suites reached only in part.
1503func TestFurtherBoundaries(t *testing.T) {
1504	uassert.ErrorContains(t, ValidateLabel(" "), "nothing visible")
1505	uassert.Error(t, ValidateLabel("a\U000E01EF"))
1506	uassert.Error(t, ValidateEndpoint(RPC, "https://h.example.com/a%2!"))
1507	uassert.Error(t, ValidateEndpoint(RPC, "https://h.example.com/%2d"))
1508	uassert.Error(t, ValidateEndpoint(RPC, "https://1.2.3.4.5"))
1509	uassert.Error(t, ValidateEndpoint(Peer, nodeID+"@"+strings.Repeat("a", 250)+".example.com:26656"))
1510	uassert.Equal(t, 16, ReservedForReviewers)
1511
1512	r := NewRegistry()
1513	in := onyx()
1514	in.ChainID = " onyx-1 "
1515	uassert.NoError(t, r.Propose(alice, 1, "onyx", in))
1516	z, _ := r.Zone("onyx")
1517	uassert.Equal(t, "onyx-1", z.ChainID)
1518	uassert.NoError(t, r.ReviewZone("onyx", Rejected, rev(r, "onyx"), curator, 2, "first"))
1519	uassert.NoError(t, r.ReviewZone("onyx", Rejected, rev(r, "onyx"), carol, 9, "restated"))
1520	z, _ = r.Zone("onyx")
1521	uassert.Equal(t, int64(9), z.ReviewedAt, "a restatement records when")
1522}
1523
1524// Round 13: rules that had no test of their own.
1525func TestRoundThirteenBoundaries(t *testing.T) {
1526	// HasVisible is exported, so its filler rule is its own, not checkText's.
1527	uassert.False(t, HasVisible("\u3164"), "a Hangul filler is not visible")
1528	uassert.True(t, HasVisible("a\u3164"))
1529	// A 0x last label is held to the address rule even when it is not hex.
1530	uassert.Error(t, ValidateEndpoint(RPC, "https://a.0xyz"))
1531	// A peer is at most MaxURLLen bytes, exactly.
1532	peer := func(n int) string {
1533		host := strings.Repeat("a", 63) + "." + strings.Repeat("b", 63) + "." + strings.Repeat("c", 63) + "."
1534		pre, post := nodeID+"@", ".com:26656"
1535		return pre + host + strings.Repeat("d", n-len(pre)-len(host)-len(post)) + post
1536	}
1537	uassert.Equal(t, MaxURLLen, len(peer(MaxURLLen)))
1538	uassert.NoError(t, ValidateEndpoint(Peer, peer(MaxURLLen)))
1539	uassert.Error(t, ValidateEndpoint(Peer, peer(MaxURLLen+1)))
1540	// A filtered list is capped at its length, so an importer's append copies
1541	// instead of writing into a slice it may not write.
1542	r := NewRegistry()
1543	uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1544	for i := 0; i < 3; i++ {
1545		_, err := r.Register(alice, 1, "onyx", RPC, "https://r"+strconv.Itoa(i)+".example.com", "")
1546		uassert.NoError(t, err)
1547	}
1548	id, err := r.Register(alice, 1, "onyx", RPC, "https://v.example.com", "")
1549	uassert.NoError(t, err)
1550	e, _ := r.Endpoint(id)
1551	z, _ := r.Zone("onyx")
1552	uassert.NoError(t, r.ReviewEndpoint(id, Verified, z.Revision, e.Revision, alice, 1, ""))
1553	for _, es := range [][]Endpoint{
1554		r.Endpoints(EndpointFilter{Zone: "onyx", Status: Verified}),
1555		r.Endpoints(EndpointFilter{Zone: "onyx", Kind: RPC, Status: Verified}),
1556		r.Endpoints(EndpointFilter{Status: Verified}),
1557	} {
1558		uassert.Equal(t, 1, len(es))
1559		uassert.Equal(t, len(es), cap(es))
1560	}
1561	uassert.NoError(t, r.Propose(alice, 1, "dev", Info{ChainID: "dev", Title: "D", Kind: Devnet, RPCURL: "https://rpc.dev.example.com"}))
1562	for _, zs := range [][]Zone{r.Zones(ZoneFilter{Kind: Devnet}), r.Zones(ZoneFilter{Status: Pending, Kind: Devnet})} {
1563		uassert.Equal(t, 1, len(zs))
1564		uassert.Equal(t, len(zs), cap(zs))
1565	}
1566}