| 1 | package hostlife |
| 2 | |
| 3 | import ( |
| 4 | "crypto/sha256" |
| 5 | "fmt" |
| 6 | "hash/fnv" |
| 7 | "os" |
| 8 | ) |
| 9 | |
| 10 | var _ = fnv.New32a // rev 1's stand-in hash, kept importable |
| 11 | |
| 12 | // Two boxes per region (region HA spec 1 §9 L18; claim rev 6.30 §11). |
| 13 | // |
| 14 | // boxesPerRegion is the world's shape: one box per region, today's model, |
| 15 | // where every seed is byte-identical to the one-box baseline (spec §4: a |
| 16 | // region with one box behaves exactly as today); or two, where every |
| 17 | // region starts with a second enrolled box and the placement, relocation |
| 18 | // and termination rules below apply. HOSTLIFE_BOXES=2 selects the two-box |
| 19 | // shape for a sweep (mutate.sh inherits it); TestHostLifecycleTwoBox sets |
| 20 | // it for its own run. A two-box world draws its extra random values only |
| 21 | // after the one-box draws of the same step, so the one-box stream is |
| 22 | // untouched. |
| 23 | var boxesPerRegion = 1 |
| 24 | |
| 25 | func init() { |
| 26 | if os.Getenv("HOSTLIFE_BOXES") == "2" { |
| 27 | boxesPerRegion = 2 |
| 28 | } |
| 29 | } |
| 30 | |
| 31 | // regionBoxes is every live box of region r, by id. |
| 32 | func (w *world) regionBoxes(r int) []*box { |
| 33 | var out []*box |
| 34 | for _, b := range w.boxes { |
| 35 | if b.Region == r && !b.Destroyed { |
| 36 | out = append(out, b) |
| 37 | } |
| 38 | } |
| 39 | return out |
| 40 | } |
| 41 | |
| 42 | // addBox enrolls a second box in region r of a witness world: the secrets |
| 43 | // key held, no faults, id nRegions+r (the sweep's second boxes carry the |
| 44 | // same ids; replacements are 100 and up). |
| 45 | func (w *world) addBox(r int) *box { |
| 46 | b := &box{ID: nRegions + r, Region: r, HasKey: true, Enrolled: true, DeleteOnTermination: true} |
| 47 | b.forget() |
| 48 | w.boxes = append(w.boxes, b) |
| 49 | w.logf("witness: box %d enrolled in region %d", b.ID, r) |
| 50 | return b |
| 51 | } |
| 52 | |
| 53 | // tieHash is the model's stand-in for claim P5-D2's SHA-256 of |
| 54 | // host + "\x00" + box reference: a fixed function of the reference, never |
| 55 | // a draw, so the choice is the same on every replay. |
| 56 | func tieHash(id int) uint32 { return tieHashFor(host, id) } |
| 57 | |
| 58 | // tieHashFor is claim P5-D2's tie-break for the named host (rev 4, Astra |
| 59 | // R1-8 and 1b-next R18-2): the first four bytes of SHA-256(host + "\x00" + |
| 60 | // box reference), so two hostnames on equal loads may choose different |
| 61 | // boxes. The mutation of tie-hash-host hashes a constant name, as the |
| 62 | // round-18 chooser bug did. |
| 63 | func tieHashFor(hostName string, id int) uint32 { |
| 64 | if !on("tie-hash-host") { |
| 65 | hostName = host |
| 66 | } |
| 67 | sum := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%d", hostName, id))) |
| 68 | return uint32(sum[0])<<24 | uint32(sum[1])<<16 | uint32(sum[2])<<8 | uint32(sum[3]) |
| 69 | } |
| 70 | |
| 71 | // hostsOn counts the instances of app placed on b (app "" counts every |
| 72 | // app): the claimed host on its executor, or on the box its create or |
| 73 | // move named. |
| 74 | func (w *world) hostsOn(b *box, app string) int { |
| 75 | n := 0 |
| 76 | if w.h2.Kind != "" && w.h2.Box == b.ID && (app == "" || w.h2.App == app) { |
| 77 | n = 1 // the second host, placed by its create (Task 5) |
| 78 | } |
| 79 | c := w.claim |
| 80 | if c == nil || c.Holder == nil || (app != "" && c.Holder.App != app) { |
| 81 | return n |
| 82 | } |
| 83 | switch { |
| 84 | case c.Op.Kind == "creating" && c.Op.NewBox == b.ID: |
| 85 | return n + 1 |
| 86 | case c.Op.Exec == b.ID: |
| 87 | return n + 1 |
| 88 | } |
| 89 | return n |
| 90 | } |
| 91 | |
| 92 | // owesLocal: an open park-local chore names b. |
| 93 | func (w *world) owesLocal(c *claim, b *box) bool { |
| 94 | if c == nil { |
| 95 | return false |
| 96 | } |
| 97 | for _, o := range c.Obl { |
| 98 | if o.Kind == "park-local" && o.Box == b.ID { |
| 99 | return true |
| 100 | } |
| 101 | } |
| 102 | return false |
| 103 | } |
| 104 | |
| 105 | // chooseBox is region HA §5.2's choose() (H12, L8; a move's target under |
| 106 | // L15) with nothing excluded. |
| 107 | func (w *world) chooseBox(r int, app string) *box { return w.chooseBoxExcept(r, app) } |
| 108 | |
| 109 | // chooseBoxExcept is choose() over the region's eligible boxes minus |
| 110 | // except (a relocation's source, its replaced targets): among the |
| 111 | // eligible boxes of region r (§2: live, running the claim's executable, |
| 112 | // holding the secrets key, not on lost, not owing a park-local for the |
| 113 | // host, enrolled: an unenrolled reference is never eligible, HA7; rule |
| 114 | // reloc-enrolled-only, Task 3), prefer one running no instance of app, |
| 115 | // then the fewest hosts, then the greatest tieHash. With one box in the |
| 116 | // region it is that box, as today (spec §4). It is a pure function of the |
| 117 | // world; the caller records the choice in the claim's intent and never |
| 118 | // substitutes (rule create-choice-once). |
| 119 | func (w *world) chooseBoxExcept(r int, app string, except ...int) *box { |
| 120 | return w.chooseBoxFor(host, r, app, except...) |
| 121 | } |
| 122 | |
| 123 | // chooseBoxFor is choose() for the named host (its tie-break hashes that |
| 124 | // name). The sole-box shortcut, today's behaviour, is the one-box shape's |
| 125 | // alone: with region HA on, one remaining candidate is checked like any |
| 126 | // other (rev 4, Astra R1-3; rule ha-sole-box-eligibility). |
| 127 | func (w *world) chooseBoxFor(hostName string, r int, app string, except ...int) *box { |
| 128 | bs := w.regionBoxes(r) |
| 129 | if len(bs) <= 1 && (w.perRegion <= 1 || !on("ha-sole-box-eligibility")) { |
| 130 | if len(bs) == 0 || contains(except, bs[0].ID) { |
| 131 | return nil |
| 132 | } |
| 133 | return bs[0] |
| 134 | } |
| 135 | c := w.claim |
| 136 | var best *box |
| 137 | var bestKey [3]int |
| 138 | for _, b := range bs { |
| 139 | if contains(except, b.ID) || b.Old || !b.HasKey || (c != nil && contains(c.Lost, b.ID)) { |
| 140 | continue |
| 141 | } |
| 142 | if w.owesLocal(c, b) && (len(except) == 0 || on("reloc-replace-not-owing")) { |
| 143 | continue // §2: a box owing a park-local for the host is never chosen (the mutation of reloc-replace-not-owing lifts it for a relocation's target) |
| 144 | } |
| 145 | if on("reloc-enrolled-only") && !b.Enrolled { |
| 146 | continue |
| 147 | } |
| 148 | key := [3]int{w.hostsOn(b, app), w.hostsOn(b, ""), -int(tieHashFor(hostName, b.ID))} |
| 149 | if best == nil || key[0] < bestKey[0] || key[0] == bestKey[0] && (key[1] < bestKey[1] || key[1] == bestKey[1] && key[2] < bestKey[2]) { |
| 150 | best, bestKey = b, key |
| 151 | } |
| 152 | } |
| 153 | return best |
| 154 | } |
| 155 | |
| 156 | // adoptionOwner is the box adoption runs on (claim P5-D2, region HA §9 K5): |
| 157 | // in the one-box shape it is the region's box, as today; in the two-box |
| 158 | // shape exactly one live box must hold the legacy host's local state (its |
| 159 | // row or database), and zero or several refuse adoption, even when only |
| 160 | // one box of the region is left (placement does not prove ownership). |
| 161 | func (w *world) adoptionOwner(r int) *box { |
| 162 | bs := w.regionBoxes(r) |
| 163 | if w.perRegion <= 1 { |
| 164 | if len(bs) == 0 { |
| 165 | return nil |
| 166 | } |
| 167 | return bs[0] |
| 168 | } |
| 169 | var owner *box |
| 170 | for _, b := range bs { |
| 171 | holds := b.Row != nil && b.Row.Inc == w.rec.Inc || b.DB != nil && b.DB.Inc == w.rec.Inc |
| 172 | if !holds { |
| 173 | continue |
| 174 | } |
| 175 | if owner != nil { |
| 176 | return nil // several proven owners: refused |
| 177 | } |
| 178 | owner = b |
| 179 | } |
| 180 | return owner |
| 181 | } |
| 182 | |
| 183 | // ownsObligation: b may run chore o. A park-local is its box's alone; a |
| 184 | // regional chore is any live box of its region's (claim P6-D5: regional |
| 185 | // workers), as it was the region's one box's. |
| 186 | func (w *world) ownsObligation(o obligation, b *box) bool { |
| 187 | if o.Kind == "park-local" { |
| 188 | return w.ownerOf(o) == b |
| 189 | } |
| 190 | if o.Owner != "" { |
| 191 | return o.Owner == boxOwner(b.ID) && !b.Destroyed // box:<b>, the legacy owner form: that box alone, until a fence or a declared loss passes it to the region |
| 192 | } |
| 193 | return b.Region == o.Region && !b.Destroyed |
| 194 | } |
| 195 | |