proposal_test.gno
19.74 Kb · 616 lines
1package validators
2
3import (
4 "strconv"
5 "testing"
6
7 "gno.land/p/nt/testutils/v0"
8 "gno.land/p/nt/uassert/v0"
9 "gno.land/p/nt/urequire/v0"
10 sysparams "gno.land/r/sys/params"
11)
12
13// seedCache populates valoperCache with the given (op, pubkey, kr)
14// tuples — used in tests to satisfy NewValidatorProposalRequest's
15// creation-time membership check without going through valopers.
16// asGovDAOProxy is the realm these tests stand in as before invoking an
17// executor directly. SimpleExecutor.Execute is invocable only from the
18// r/gov/dao namespace; on the live route dao.ExecuteProposal and
19// impl.ExecuteProposal thread the proxy's cur to it non-crossing, so
20// Previous() inside Execute is this path. Calling Execute from the test's
21// own realm is a route no transaction takes.
22const asGovDAOProxy = "gno.land/r/gov/dao"
23
24func seedCache(t *testing.T, entries []struct {
25 op address
26 pubKey string
27 keepRunning bool
28}) {
29 t.Helper()
30 for _, e := range entries {
31 signingAddr := mustAddr(t, e.pubKey)
32 valoperCache.Set(e.op.String(), cacheEntry{
33 SigningPubKey: e.pubKey,
34 SigningAddress: signingAddr,
35 KeepRunning: e.keepRunning,
36 })
37 }
38}
39
40func TestNewValidatorProposalRequest_RejectsUnknownOperator(cur realm, t *testing.T) {
41 resetCache()
42
43 op := testutils.TestAddress("ghost-op")
44
45 uassert.PanicsContains(t, cur, "unknown operator", func() {
46 _ = NewValidatorProposalRequest(cur,
47 []ValoperChange{{OperatorAddress: op, Power: 1}},
48 "add ghost",
49 "",
50 )
51 })
52}
53
54func TestNewValidatorProposalRequest_RejectsEmptyChanges(cur realm, t *testing.T) {
55 resetCache()
56
57 uassert.PanicsContains(t, cur, errNoValoperChanges, func() {
58 _ = NewValidatorProposalRequest(cur, nil, "title", "")
59 })
60}
61
62func TestNewValidatorProposalRequest_RejectsEmptyTitle(cur realm, t *testing.T) {
63 resetCache()
64 op := testutils.TestAddress("op-A")
65 seedCache(t, []struct {
66 op address
67 pubKey string
68 keepRunning bool
69 }{{op: op, pubKey: pubKeyA, keepRunning: true}})
70
71 uassert.PanicsContains(t, cur, "proposal title is empty", func() {
72 _ = NewValidatorProposalRequest(cur,
73 []ValoperChange{{OperatorAddress: op, Power: 1}},
74 " ",
75 "",
76 )
77 })
78}
79
80func TestNewValidatorProposalRequest_RejectsTooManyChanges(cur realm, t *testing.T) {
81 resetCache()
82
83 // Seed 41 cache entries so the membership check passes; the
84 // length cap should fire before the per-entry validation.
85 changes := make([]ValoperChange, 41)
86 pubkeys := []string{pubKeyA, pubKeyB, pubKeyC}
87 for i := 0; i < 41; i++ {
88 op := testutils.TestAddress("op-" + strconv.Itoa(i))
89 pk := pubkeys[i%3]
90 valoperCache.Set(op.String(), cacheEntry{
91 SigningPubKey: pk,
92 SigningAddress: mustAddr(t, pk),
93 KeepRunning: true,
94 })
95 changes[i] = ValoperChange{OperatorAddress: op, Power: 1}
96 }
97
98 uassert.PanicsContains(t, cur, "max number of allowed validators per proposal is 40", func() {
99 _ = NewValidatorProposalRequest(cur, changes, "too many", "")
100 })
101}
102
103func TestNewValidatorProposalRequest_DescriptionRendering(cur realm, t *testing.T) {
104 resetCache()
105 opA := testutils.TestAddress("op-A")
106 opB := testutils.TestAddress("op-B")
107 seedCache(t, []struct {
108 op address
109 pubKey string
110 keepRunning bool
111 }{
112 {op: opA, pubKey: pubKeyA, keepRunning: true},
113 {op: opB, pubKey: pubKeyB, keepRunning: true},
114 })
115
116 pr := NewValidatorProposalRequest(cur,
117 []ValoperChange{
118 {OperatorAddress: opA, Power: 5},
119 {OperatorAddress: opB, Power: 0},
120 },
121 "mixed changes",
122 "context line",
123 )
124
125 desc := pr.Description()
126 urequire.True(t, len(desc) > 0)
127 uassert.True(t, contains(desc, "context line"))
128 uassert.True(t, contains(desc, "## Validator Updates"))
129 uassert.True(t, contains(desc, opA.String()+": add (power 5)"))
130 uassert.True(t, contains(desc, opB.String()+": remove"))
131}
132
133func TestNewValidatorProposalRequest_ExecutorReResolvesPubkey(cur realm, t *testing.T) {
134 // Creation-time captured changes: ValoperChange refers to opA.
135 // Cache for opA points to pubKeyA at creation. Before execution,
136 // opA's cache entry is updated to pubKeyB. Executor must publish
137 // the NEW pubkey, not the creation-time one.
138 resetValset(t)
139 resetCache()
140
141 opA := testutils.TestAddress("op-A")
142 seedCache(t, []struct {
143 op address
144 pubKey string
145 keepRunning bool
146 }{{op: opA, pubKey: pubKeyA, keepRunning: true}})
147
148 changes := []ValoperChange{{OperatorAddress: opA, Power: 7}}
149
150 // Build the executor; it captures `changes` by reference (slice
151 // of structs) but resolves SigningPubKey at run-time via cache.
152 exec := newValoperChangeExecutor(cur, changes)
153
154 // Simulate operator rotation: opA's cache entry now points to
155 // pubKeyB. Captured changes slice is unchanged.
156 valoperCache.Set(opA.String(), cacheEntry{
157 SigningPubKey: pubKeyB,
158 SigningAddress: mustAddr(t, pubKeyB),
159 KeepRunning: true,
160 })
161
162 testing.SetRealm(testing.NewCodeRealm(asGovDAOProxy))
163
164 urequire.NoError(t, exec.Execute(cross(cur)))
165
166 // Effective valset should contain pubKeyB (post-rotation), not
167 // pubKeyA (creation-time).
168 effective := sysparams.GetValsetEffective()
169 urequire.Equal(t, 1, len(effective))
170 uassert.Equal(t, pubKeyB, effective[0].PubKey)
171 uassert.Equal(t, uint64(7), effective[0].VotingPower)
172}
173
174func TestNewValidatorProposalRequest_RemoveOperator(cur realm, t *testing.T) {
175 resetValset(t)
176 resetCache()
177
178 // Seed valset with opA already signing under pubKeyA.
179 testing.SetSysParamStrings(module, submodule, currKey, []string{pubKeyA + ":10"})
180
181 opA := testutils.TestAddress("op-A")
182 seedCache(t, []struct {
183 op address
184 pubKey string
185 keepRunning bool
186 }{{op: opA, pubKey: pubKeyA, keepRunning: false}})
187
188 // Liveness floor: removing the only validator empties the set.
189 // Executor runs inside a crossing dao.Executor.Execute call, so
190 // the panic surfaces as an abort, not a regular panic.
191 testing.SetRealm(testing.NewCodeRealm(asGovDAOProxy))
192
193 uassert.AbortsContains(t, cur, "would empty the validator set", func() {
194 _ = newValoperChangeExecutor(cur, []ValoperChange{{OperatorAddress: opA, Power: 0}}).Execute(cross(cur))
195 })
196}
197
198func TestNewValidatorProposalRequest_RemoveLeavesOthers(cur realm, t *testing.T) {
199 resetValset(t)
200 resetCache()
201
202 // Seed valset with two validators.
203 testing.SetSysParamStrings(module, submodule, currKey, []string{
204 pubKeyA + ":10",
205 pubKeyB + ":5",
206 })
207
208 opA := testutils.TestAddress("op-A")
209 opB := testutils.TestAddress("op-B")
210 seedCache(t, []struct {
211 op address
212 pubKey string
213 keepRunning bool
214 }{
215 {op: opA, pubKey: pubKeyA, keepRunning: true},
216 {op: opB, pubKey: pubKeyB, keepRunning: true},
217 })
218
219 changes := []ValoperChange{{OperatorAddress: opA, Power: 0}}
220 // Build the executor directly (private function, same package).
221 testing.SetRealm(testing.NewCodeRealm(asGovDAOProxy))
222
223 urequire.NoError(t, newValoperChangeExecutor(cur, changes).Execute(cross(cur)))
224
225 // Effective set: only opB / pubKeyB remains.
226 effective := sysparams.GetValsetEffective()
227 urequire.Equal(t, 1, len(effective))
228 uassert.Equal(t, pubKeyB, effective[0].PubKey)
229}
230
231func TestNewValidatorProposalRequest_AllowsFullValsetReplacement(cur realm, t *testing.T) {
232 resetValset(t)
233 resetCache()
234
235 testing.SetSysParamStrings(module, submodule, currKey, []string{pubKeyA + ":10"})
236
237 opA := testutils.TestAddress("op-A")
238 opB := testutils.TestAddress("op-B")
239 seedCache(t, []struct {
240 op address
241 pubKey string
242 keepRunning bool
243 }{
244 {op: opA, pubKey: pubKeyA, keepRunning: false},
245 {op: opB, pubKey: pubKeyB, keepRunning: true},
246 })
247
248 changes := []ValoperChange{
249 {OperatorAddress: opA, Power: 0},
250 {OperatorAddress: opB, Power: 10},
251 }
252 testing.SetRealm(testing.NewCodeRealm(asGovDAOProxy))
253
254 urequire.NoError(t, newValoperChangeExecutor(cur, changes).Execute(cross(cur)))
255
256 effective := sysparams.GetValsetEffective()
257 urequire.Equal(t, 1, len(effective))
258 uassert.Equal(t, pubKeyB, effective[0].PubKey)
259 uassert.Equal(t, uint64(10), effective[0].VotingPower)
260}
261
262func TestNewValidatorProposalRequest_RejectsKeepRunningFalseAtCreation(cur realm, t *testing.T) {
263 resetCache()
264 op := testutils.TestAddress("op-A")
265 seedCache(t, []struct {
266 op address
267 pubKey string
268 keepRunning bool
269 }{{op: op, pubKey: pubKeyA, keepRunning: false}})
270
271 uassert.PanicsContains(t, cur, "KeepRunning=false", func() {
272 _ = NewValidatorProposalRequest(cur,
273 []ValoperChange{{OperatorAddress: op, Power: 1}},
274 "add opted-out", "",
275 )
276 })
277}
278
279func TestNewValidatorProposalRequest_AllowsRemoveOfKeepRunningFalse(cur realm, t *testing.T) {
280 // KeepRunning=false is the operator's opt-out signal; removing
281 // such an operator must still be allowed (it's the standard
282 // exit path). Only adds are gated.
283 resetValset(t)
284 resetCache()
285
286 // Seed the valset with two operators so removing one doesn't
287 // trip the empty-valset liveness floor.
288 testing.SetSysParamStrings(module, submodule, currKey, []string{
289 pubKeyA + ":10",
290 pubKeyB + ":5",
291 })
292
293 opA := testutils.TestAddress("op-A")
294 opB := testutils.TestAddress("op-B")
295 seedCache(t, []struct {
296 op address
297 pubKey string
298 keepRunning bool
299 }{
300 {op: opA, pubKey: pubKeyA, keepRunning: false}, // opted out
301 {op: opB, pubKey: pubKeyB, keepRunning: true},
302 })
303
304 // Build proposal succeeds (remove path: Power=0 ignores KeepRunning).
305 pr := NewValidatorProposalRequest(cur,
306 []ValoperChange{{OperatorAddress: opA, Power: 0}},
307 "remove opted-out opA", "",
308 )
309 _ = pr
310
311 // Executor also succeeds.
312 testing.SetRealm(testing.NewCodeRealm(asGovDAOProxy))
313
314 urequire.NoError(t, newValoperChangeExecutor(cur, []ValoperChange{{OperatorAddress: opA, Power: 0}}).Execute(cross(cur)))
315}
316
317func TestNewValidatorProposalRequest_RejectsDuplicateOp(cur realm, t *testing.T) {
318 // Each operator may appear at most once per proposal; any shape
319 // that mentions the same op twice must panic at create-time.
320 resetCache()
321 op := testutils.TestAddress("op-A")
322 seedCache(t, []struct {
323 op address
324 pubKey string
325 keepRunning bool
326 }{{op: op, pubKey: pubKeyA, keepRunning: true}})
327
328 cases := [][]ValoperChange{
329 {{OperatorAddress: op, Power: 0}, {OperatorAddress: op, Power: 7}}, // remove + re-add
330 {{OperatorAddress: op, Power: 7}, {OperatorAddress: op, Power: 8}}, // double add
331 {{OperatorAddress: op, Power: 0}, {OperatorAddress: op, Power: 0}}, // double remove
332 }
333 for _, changes := range cases {
334 uassert.PanicsContains(t, cur, "duplicate operator in proposal", func() {
335 _ = NewValidatorProposalRequest(cur, changes, "dup", "")
336 })
337 }
338}
339
340func TestNewValidatorProposalRequest_RejectsPowerUpdatePairForOptedOutOp(cur realm, t *testing.T) {
341 // KeepRunning=false is binding: no proposal shape can keep an
342 // opted-out operator in the active set. The dedupe rejection
343 // fires before the KR check is even reached.
344 resetCache()
345 op := testutils.TestAddress("op-A")
346 seedCache(t, []struct {
347 op address
348 pubKey string
349 keepRunning bool
350 }{{op: op, pubKey: pubKeyA, keepRunning: false}})
351
352 uassert.PanicsContains(t, cur, "duplicate operator in proposal", func() {
353 _ = NewValidatorProposalRequest(cur,
354 []ValoperChange{
355 {OperatorAddress: op, Power: 0},
356 {OperatorAddress: op, Power: 7},
357 },
358 "bypass attempt", "",
359 )
360 })
361}
362
363func TestNewValidatorProposalRequest_UpsertExistingValidator(cur realm, t *testing.T) {
364 // Single-entry {op, newPower} against an op already in the
365 // effective valset must upsert: the existing entry's power is
366 // overwritten, no remove/re-add ceremony required.
367 resetValset(t)
368 resetCache()
369
370 // Seed valset with two validators; we upsert opA's power.
371 testing.SetSysParamStrings(module, submodule, currKey, []string{
372 pubKeyA + ":1",
373 pubKeyB + ":1",
374 })
375
376 opA := testutils.TestAddress("op-A")
377 opB := testutils.TestAddress("op-B")
378 seedCache(t, []struct {
379 op address
380 pubKey string
381 keepRunning bool
382 }{
383 {op: opA, pubKey: pubKeyA, keepRunning: true},
384 {op: opB, pubKey: pubKeyB, keepRunning: true},
385 })
386
387 changes := []ValoperChange{{OperatorAddress: opA, Power: 9}}
388 testing.SetRealm(testing.NewCodeRealm(asGovDAOProxy))
389
390 urequire.NoError(t, newValoperChangeExecutor(cur, changes).Execute(cross(cur)))
391
392 effective := sysparams.GetValsetEffective()
393 powerOf := map[string]uint64{}
394 for _, v := range effective {
395 powerOf[v.PubKey] = v.VotingPower
396 }
397 uassert.Equal(t, uint64(9), powerOf[pubKeyA], "opA power upserted from 1 to 9")
398 uassert.Equal(t, uint64(1), powerOf[pubKeyB], "opB unchanged")
399}
400
401func TestNewValidatorProposalRequest_ExecutorRejectsRaceFlippedKeepRunning(cur realm, t *testing.T) {
402 // KeepRunning=true at proposal-create time; operator flips to
403 // false BEFORE the executor runs. Race-safety check rejects.
404 resetValset(t)
405 resetCache()
406
407 op := testutils.TestAddress("op-A")
408 seedCache(t, []struct {
409 op address
410 pubKey string
411 keepRunning bool
412 }{{op: op, pubKey: pubKeyA, keepRunning: true}})
413
414 changes := []ValoperChange{{OperatorAddress: op, Power: 5}}
415 exec := newValoperChangeExecutor(cur, changes)
416
417 // Operator flips KeepRunning=false BEFORE the executor runs.
418 valoperCache.Set(op.String(), cacheEntry{
419 SigningPubKey: pubKeyA,
420 SigningAddress: mustAddr(t, pubKeyA),
421 KeepRunning: false,
422 })
423
424 testing.SetRealm(testing.NewCodeRealm(asGovDAOProxy))
425
426 uassert.AbortsContains(t, cur, "KeepRunning=false at execution", func() {
427 _ = exec.Execute(cross(cur))
428 })
429}
430
431func TestNewValidatorProposalRequest_NaturalRotationFlow_NoGhost(cur realm, t *testing.T) {
432 // RotateValoperSigningKey publishes valset:proposed before any
433 // subsequent executor reads, so baseline always reflects the
434 // post-rotation state. A later power-update upserts at NEW only;
435 // no OLD ghost.
436 resetValset(t)
437 resetCache()
438
439 testing.SetSysParamStrings(module, submodule, currKey, []string{
440 pubKeyA + ":1",
441 pubKeyB + ":5",
442 })
443 opA := testutils.TestAddress("op-A")
444 opB := testutils.TestAddress("op-B")
445 seedCache(t, []struct {
446 op address
447 pubKey string
448 keepRunning bool
449 }{
450 {op: opA, pubKey: pubKeyA, keepRunning: true},
451 {op: opB, pubKey: pubKeyB, keepRunning: true},
452 })
453
454 testing.SetRealm(testing.NewCodeRealm(valopersRealmPath))
455 RotateValoperSigningKey(cross(cur), opA, pubKeyA, pubKeyC)
456 NotifyValoperChanged(cross(cur), opA, pubKeyC, mustAddr(t, pubKeyC), true)
457
458 testing.SetRealm(testing.NewCodeRealm("gno.land/r/gov/dao/impl/v0"))
459 urequire.NoError(t, newValoperChangeExecutor(cur,
460 []ValoperChange{{OperatorAddress: opA, Power: 2}},
461 ).Execute(cross(cur)))
462
463 effective := sysparams.GetValsetEffective()
464 powerOf := map[string]uint64{}
465 for _, v := range effective {
466 powerOf[v.PubKey] = v.VotingPower
467 }
468 uassert.Equal(t, uint64(2), powerOf[pubKeyC], "opA published at NEW power=2")
469 uassert.Equal(t, uint64(5), powerOf[pubKeyB], "opB unchanged")
470 _, ghost := powerOf[pubKeyA]
471 uassert.False(t, ghost, "OLD signing key (pubKeyA) must not linger in valset")
472 urequire.Equal(t, 2, len(effective), "exactly two entries — opA(NEW), opB")
473}
474
475func TestNewValidatorProposalRequest_PhantomBaselineDocumentsUnreachableState(cur realm, t *testing.T) {
476 // Pin executor behavior on a phantom state (cache=NEW,
477 // valset:current=OLD, dirty=false) — unreachable via natural
478 // flow because RotateValoperSigningKey publishes proposed
479 // before NotifyValoperChanged updates the cache. If a future
480 // code path ever updates the cache without going through
481 // Rotate, the asymmetry would be a real bug; this test pins
482 // the current behavior so the divergence surfaces.
483 resetValset(t)
484 resetCache()
485
486 testing.SetSysParamStrings(module, submodule, currKey, []string{
487 pubKeyA + ":1",
488 pubKeyB + ":5",
489 })
490
491 opA := testutils.TestAddress("op-A")
492 opB := testutils.TestAddress("op-B")
493 seedCache(t, []struct {
494 op address
495 pubKey string
496 keepRunning bool
497 }{
498 {op: opA, pubKey: pubKeyC, keepRunning: true},
499 {op: opB, pubKey: pubKeyB, keepRunning: true},
500 })
501
502 testing.SetRealm(testing.NewCodeRealm(asGovDAOProxy))
503
504 urequire.NoError(t, newValoperChangeExecutor(cur,
505 []ValoperChange{{OperatorAddress: opA, Power: 2}},
506 ).Execute(cross(cur)))
507
508 effective := sysparams.GetValsetEffective()
509 powerOf := map[string]uint64{}
510 for _, v := range effective {
511 powerOf[v.PubKey] = v.VotingPower
512 }
513 uassert.Equal(t, uint64(1), powerOf[pubKeyA], "phantom OLD lingers from baseline")
514 uassert.Equal(t, uint64(2), powerOf[pubKeyC], "executor upserts at NEW")
515 uassert.Equal(t, uint64(5), powerOf[pubKeyB], "opB unchanged")
516 urequire.Equal(t, 3, len(effective),
517 "three entries — phantom-state ghost; this state is unreachable via natural flow")
518}
519
520func TestNewValidatorProposalRequest_SameBlockExecuteThenRotate(cur realm, t *testing.T) {
521 // Same-block ordering: proposal-execute writes proposed
522 // (dirty=true); a subsequent rotation reads proposed-when-dirty
523 // and accumulates the prior power change rather than clobbering
524 // back to current.
525 resetValset(t)
526 resetCache()
527
528 testing.SetSysParamStrings(module, submodule, currKey, []string{
529 pubKeyA + ":1",
530 pubKeyB + ":5",
531 })
532
533 opA := testutils.TestAddress("op-A")
534 opB := testutils.TestAddress("op-B")
535 seedCache(t, []struct {
536 op address
537 pubKey string
538 keepRunning bool
539 }{
540 {op: opA, pubKey: pubKeyA, keepRunning: true},
541 {op: opB, pubKey: pubKeyB, keepRunning: true},
542 })
543
544 testing.SetRealm(testing.NewCodeRealm(asGovDAOProxy))
545
546 urequire.NoError(t, newValoperChangeExecutor(cur,
547 []ValoperChange{{OperatorAddress: opA, Power: 3}},
548 ).Execute(cross(cur)))
549
550 testing.SetRealm(testing.NewCodeRealm(valopersRealmPath))
551 RotateValoperSigningKey(cross(cur), opA, pubKeyA, pubKeyC)
552 NotifyValoperChanged(cross(cur), opA, pubKeyC, mustAddr(t, pubKeyC), true)
553
554 effective := sysparams.GetValsetEffective()
555 powerOf := map[string]uint64{}
556 for _, v := range effective {
557 powerOf[v.PubKey] = v.VotingPower
558 }
559 uassert.Equal(t, uint64(3), powerOf[pubKeyC], "rotation accumulates with prior upsert; final power=3")
560 uassert.Equal(t, uint64(5), powerOf[pubKeyB], "opB unchanged")
561 _, gotOld := powerOf[pubKeyA]
562 uassert.False(t, gotOld, "OLD signing key (pubKeyA) must not appear after rotation")
563 urequire.Equal(t, 2, len(effective), "exactly two entries — opA(NEW) and opB")
564}
565
566// TestNewValidatorProposalRequest_ExecutorVanishedCacheEntryPanics pins
567// the "operator vanished from valoperCache between propose and execute"
568// branch. No production path deletes from valoperCache today (only Set
569// is called by NotifyValoperChanged), but bptree.BPTree exposes Remove
570// so the underlying data structure does support deletion. This test
571// simulates that hypothetical state directly via the package-private
572// cache var to confirm the executor panics with the documented message
573// rather than silently mis-publishing an empty/wrong valset.
574//
575// If a future commit ever introduces a public cache-delete path, this
576// test still passes — it's a contract-pinning test for the panic itself.
577// If the team decides the branch is unreachable enough to drop, this
578// test is the first thing to break.
579func TestNewValidatorProposalRequest_ExecutorVanishedCacheEntryPanics(cur realm, t *testing.T) {
580 resetValset(t)
581 resetCache()
582
583 op := testutils.TestAddress("op-A")
584 seedCache(t, []struct {
585 op address
586 pubKey string
587 keepRunning bool
588 }{{op: op, pubKey: pubKeyA, keepRunning: true}})
589
590 exec := newValoperChangeExecutor(cur,
591 []ValoperChange{{OperatorAddress: op, Power: 5}},
592 )
593
594 // Simulate the unreachable-today state: cache entry deleted between
595 // proposal-create and proposal-execute. Direct package-private mutation
596 // (NOT a public API) — production code has no path here today.
597 _, removed := valoperCache.Remove(op.String())
598 urequire.True(t, removed, "fixture: cache entry must have been present before Remove")
599
600 testing.SetRealm(testing.NewCodeRealm(asGovDAOProxy))
601
602 uassert.AbortsContains(t, cur, "operator vanished from valoperCache between propose and execute", func() {
603 _ = exec.Execute(cross(cur))
604 })
605}
606
607// contains is a tiny strings.Contains shim so tests don't import a
608// new package.
609func contains(s, substr string) bool {
610 for i := 0; i+len(substr) <= len(s); i++ {
611 if s[i:i+len(substr)] == substr {
612 return true
613 }
614 }
615 return false
616}